Skip to content

Commit c8a8ba1

Browse files
authored
More concepts documentation (#946)
More work on #666
1 parent a1e9abb commit c8a8ba1

File tree

2 files changed

+128
-2
lines changed

2 files changed

+128
-2
lines changed

docs/concepts.rst

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -927,8 +927,132 @@ Suppose you wanted to serialize :external:py:class:`datetime.datetime` instances
927927
Data Types and Records
928928
----------------------
929929

930-
TBD
930+
Basilisp allows 3 different methods for defining custom data types which implement Python interfaces and :ref:`protocols`, detailed in the sections below.
931+
932+
Each of the methods Basilisp supports for creating custom data types may implement 0 or more Python interfaces and Basilisp protocols.
933+
Types are required to implement every function defined by any declared interfaces and protocols.
934+
935+
Types may also optionally implement 0 or more Python `"dunder" methods <https://docs.python.org/3/reference/datamodel.html>`_ without implementing every such method.
936+
937+
.. note::
938+
939+
It is not necessary to declare :external:py:class:`object` as a superclass, but doing so is not an error.
940+
941+
.. warning::
942+
943+
Python, unlike Java, does not have a true "interface" type.
944+
The best approximation is :external:py:class:`abc.ABC`, although this type is merely advisory and many libraries and applications eschew its use.
945+
946+
For the cases where a host type is not defined as an ``abc.ABC`` instance, users can override the compiler check by setting the ``^:abstract`` meta key on the interface type symbol passed to the ``deftype`` form.
947+
For example, take :external:py:class:`argparse.Action` which is required to be implemented for customizing :external:py:mod:`argparse` actions, but which is not defined as an ``abc.ABC``:
948+
949+
.. code-block::
950+
951+
(import argparse)
952+
953+
(reify
954+
^:abstract argparse/Action
955+
(__call__ [this]
956+
;; ...
957+
))
958+
959+
.. _deftype:
960+
961+
``deftype``
962+
^^^^^^^^^^^
963+
964+
In many cases it is desirable or necessary to define a Python class (or object instance which is a subtype of some type) to interact with a Python library.
965+
To facilitate this, Basilisp includes the :lpy:fn:`deftype` macro for creating Python classes which optionally implement Python interfaces or Basilisp protocols.
966+
967+
Types defined via ``deftype`` may include 0 or more fields which are required at object instantiation.
968+
Fields defined in ``deftype`` forms are immutable by default.
969+
Attempting to set a field using :lpy:form:`set!` will result in a compile-time error.
970+
However, it is possible to mark a field as mutable by using the ``^:mutable`` metadata on a ``deftype`` field at compile time.
971+
Mutable fields may be ``set!`` from within class methods.
972+
Fields may be referred to freely by name from within method definitions as in Java (and unlike in Python where they must be qualified with ``self``).
973+
Fields may also specify defaults by providing the default value as a ``^:default`` metadata value.
974+
975+
.. warning::
976+
977+
Python is known for taking a rather lax stance on object mutability as compared to many other languages and runtimes.
978+
As a consequence of the language and VM not enforcing true immutability, even immutable fields may still be modified by other means.
979+
Users should not take the immutable default state of ``deftype`` fields as a guarantee, but rather as a principled approach to reducing the surface area of potential bugs due to mutability.
980+
981+
Types created by ``deftype`` automatically have some basic sensible defaults added via `attrs <https://www.attrs.org/en/stable/>`_, such as a constructor (whose argument order matches that of the defined fields) and Python ``__str__`` and ``__repr__`` methods.
982+
User supplied versions of methods besides ``__init__`` may override the generated variants in all cases.
983+
984+
Methods may be defined with multiple arities if required by any declared protocols.
985+
``deftype`` methods may be :ref:`defined with support for Python kwargs <basilisp_functions_with_kwargs>` exactly like plain functions.
986+
Methods may be declared as by :external:py:func:`classmethod` and :external:py:func:`staticmethod` using the ``^:classmethod`` and ``^:staticmethod`` metadata respectively on the method name.
987+
Static and classmethods may be defined with multiple arities.
988+
Methods may also be declared as properties as by :external:py:class:`property` using the ``^:property`` metadata on the method name.
989+
Property methods must be single arity.
990+
991+
Given a new type ``deftype`` named ``Point``, a new constructor function ``->Point`` will be created alongside the record type which accepts the full set of declared fields in the order they are declared.
992+
993+
.. note::
994+
995+
Method definitions must include a ``self`` or ``this`` parameter.
996+
997+
.. note::
998+
999+
Methods support tail recursion via :lpy:form:`recur`.
1000+
When recurring, users should *not* pass the ``this`` or ``self`` parameter.
1001+
1002+
.. _reify:
1003+
1004+
``reify``
1005+
^^^^^^^^^
1006+
1007+
Whereas :ref:`deftype` defines a true Python class which may be instantiated directly, :lpy:fn:`reify` defines an anonymous type implementing the named interfaces and protocols and returns an instance of that type immediately.
1008+
Types defined via ``reify`` may not include fields.
1009+
Instead, reified types close over their environment, which can provide many of the same benefits as fields.
1010+
1011+
Reify is likely to be most useful for creating one-off types implementing some Python type, rather than for creating types that are going to be created and used frequently by consumers of your code.
1012+
1013+
Reified types always implement :py:class:`basilisp.lang.interfaces.IWithMeta` and any metadata applied to the ``reify`` form are transferred to the created object.
1014+
1015+
.. note::
1016+
1017+
While ``reify`` and ``deftype`` are broadly similar, ``reify`` types may not define class or static methods.
1018+
1019+
.. _defrecord:
1020+
1021+
``defrecord``
1022+
^^^^^^^^^^^^^
1023+
1024+
Basilisp offers a record type, created via :lpy:fn:`defrecord`, which is broadly similar to the types created by :ref:`deftype`.
1025+
Record types are designed to be object types which can interact more readily with the core Basilisp library as a result of implementing the map interface directly.
1026+
Records may be created from maps and fields in may be accessed, updated, and removed using standard :ref:`map <maps>` library functions.
1027+
1028+
There are some key differences from ``deftype`` types, however.
1029+
1030+
- Record types automatically implement :py:class:`basilisp.lang.interfaces.IPersistentMap`, :py:class:`basilisp.lang.interfaces.IWithMeta`, :py:class:`basilisp.lang.interfaces.IRecord`, and support for equality and hashing implemented via Python ``object`` methods.
1031+
- ``defrecord`` fields may not be marked ``^:mutable``, nor may they provide a default via ``^:default``.
1032+
- Types created by ``defrecord`` may not include :external:py:func:`classmethod`, :external:py:class:`property`, or :external:py:func:`staticmethod` methods.
1033+
- Given a defrecord type ``Point``, a constructor function ``map->Point`` will be created alongside the record type which can construct a new ``Point`` record from a map in addition to the positional constructor ``->Point``.
1034+
- Record literals may be constructed using their fully-qualified name as a :ref:`data reader <data_readers>` using a vector literal for a positional constructor or a map for a map based constructor.
1035+
1036+
.. code-block::
1037+
1038+
(defrecord Point [x y z])
1039+
(->Point 1 2 3) ;; => #basilisp.user.Point{:z 3 :x 1 :y 2}
1040+
(def p (map->Point {:x 1 :y 2 :z 3})) ;; => #basilisp.user.Point{:z 3 :x 1 :y 2}
1041+
(:x p) ;; => 1
1042+
(dissoc p :x) ;; => {:z 3 :y 2}
1043+
1044+
(def p1 (assoc p :name "Best point")) ;; => #basilisp.user.Point{:z 3 :x 1 :name "Best point" :y 2}
1045+
(dissoc p1 :name) ;; => #basilisp.user.Point{:z 3 :x 1 :y 2}
1046+
1047+
#basilisp.user.Point[4 5 6] ;; => #basilisp.user.Point{:z 6 :x 4 :y 5}
1048+
#basilisp.user.Point{:x 4 :y 5 :z 6} ;; => #basilisp.user.Point{:z 6 :x 4 :y 5}
1049+
1050+
.. note::
1051+
1052+
Users may add arbitrary extra fields onto a record (as by :lpy:fn:`assoc`) without changing its type.
1053+
If a field required by the record definition is removed as by :lpy:fn:`dissoc`, the record type will be downgraded to a standard map.
1054+
Extra fields which are not part of the record may be removed without changing the type.
9311055

9321056
.. seealso::
9331057

934-
:lpy:fn:`deftype`, :lpy:fn:`defrecord` , :lpy:fn:`record?`
1058+
:lpy:fn:`deftype`, :lpy:fn:`defrecord`, :lpy:fn:`reify`, :lpy:fn:`record?`

docs/pyinterop.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ The ``:collect`` strategy is a better accompaniment to functions with positional
212212
With this strategy, Python keyword arguments are converted into a Basilisp map with de-munged keyword arguments and passed as the final positional argument of the function.
213213
You can use :ref:`associative_destructuring` on this final positional argument, just as you would with the map in the ``:apply`` case above.
214214

215+
.. _type_hinting:
216+
215217
Type Hinting
216218
------------
217219

0 commit comments

Comments
 (0)