I want to write some generic code dealing with reflection groups, and therefore need to set up some types which reflect mathematical structures (vector space, affine space, ...). As I really want to reflect those structures faithfully in types, I need a way to define some kind of parametric type.
So in particular, I'd want to be able to write the following code
(defclass RealVectorSpace ()
((V :accessor underlying-set
:type Set)
(vector-add :accessor add
:type (function ((set-as-type V) (set-as-type V)) (set-as-type V)))
(scalar-mult :accessor s-mult
:type (function (real (set-as-type V)) (set-as-type V)))
which should specify a new type RealVectorSpace which would be given by a triple (V vector-add scalar) where V can be anything, and vector-add is a function taking two parameters of type V (sic) that evaluates to something of type V.
Of course, this type wouldn't be quite a faithful reflection of the concept of a real vector space, because vector-add and scalar-mult would still need to satisfy some further properties. But even turning that 'dream' above into real code eludes me.
Edit: In response to sds's answer, let me put forward the following clarification of my original question: in a nutshell, it seems I need to have dependent types in Lisp, which is different from asking just for parametric classes. In fact, Haskell has parametric types but does not have (at least it's not built-in in an obvious way) dependent types. The lack of dependent types in Haskell is for instance discussed here.
1. Can anyone help me turn my dream into code?
2. I've heard somewhere that you don't need parametric classes in Lisp because of Lisp macros. If that's true, can someone explain how you'd use defmacro to implement/fake parametric classes in Common Lisp?
Here we go: a partial answer/solution to my question (why partial? see below). Many thanks to sds for helping me figure this out!
Let me begin by making a clarification. When I posed my question initially, I used the term 'parametric type' imprecisely, having only a vague definition as 'a type depending parameters' in mind. I basically wanted some gadget allowing me to write the following pseudo-code (in a pseudo-language):
Making sense of the above pseudo-code is pretty straight-forward (see sds's answer). However, ambiguity arises if one starts to wonder whether the expressions
List<T>
andList
themselves should have a meaning. Indeed, in C++ for example, the expressions would be undefined, for the effect of the template definitionis as if defining separately, for each type
T
, a typeList<T>
. In contrast, in languages like Java that implement generic types, the expressionList<T>
(whereT
is a free variable) makes sense and denotes a type, namely the generic type of a list over some typeT
, so that one could e.g. write a polymorphic function likeIn short, in C++ we only have the (infinite) collection of all types
List<T>
whereT
runs over all types, whereas in Java this collection itself exists as an object in the language, as the generic typeList<T>
.Now to my proposed partial solution, which I will briefly sketch in words before writing the actual Lisp code. The solution is based on type families and the dependent sum of such families, i.e. we are going to interpret a parametric type like the type
List<T>
above as a functionList
of one argument whose values are types, and we fake the Java-style generic typeList<T>
as the dependent sum typeDepSum(List)
, which simply consists of pairs(a,b)
wherea
is some type andb
is of typeList(b)
.Returning to the example of defining a real vector space over a set
X
, I would like to write something likedefining for me a function RealVectorSpaceOver that, given a class
A
, would return a class as if defined manually byBasically, I could copy-n-paste the solution of sds here, but there are two problems with this. First of all, the result would not be a (side-effect-free) function, for example the form
would evaluate to
nil
because there are two calls toRealVectorSpaceOver
here, and each call creates a new (and therefore different) class. Thus, we need to wrap this function in some code that caches the result once it has been called for the first time.The other problem is that using
defclass
to create classes programmatically has the effect of changing the class name space, possibly redefining existing classes. To avoid this, one can instead create new classes directly by instantiating the meta classstandard-class
. For exampleis equivalent to
but wouldn't redefine any pre-existing class
B
. Note that the format for the:direct-slots
argument differs slightly from thedefclass
format for defining slots. Using a helper functioncanonicalize-direct-slot-defs
that transforms the latter into the former (taken from the book 'The Art of the Metaobject Protocol'), the macrodefclassfamily
can be implemented as follows:The above code first defines a function
f
representing the desired type family, then creates a cached-versiong
of it using a helper functioncache-function
(insert your own implementation), and then defines a new function in the name space usingdefun
, enforcing the types for the arguments (defclassfamily
accepts typed arguments similar todefmethod
, so that(defclassfamily F ((X Set) Y) ...
would define a familyF
of two parameters, with the first one being of typeSet
) and the return value of the class family. Moreover, there are some simple helper functionstrip-variables
,types-of-variables
, andtype-decls-from-variables
that transform the expression given the variables of the type family ((X Set) Y
in the previous example). They are defined as follows:Finally, we record the types of the arguments taken by our family using a method
argument-signature
, so thatwould evaluate to
(Set t)
.The dependent sum of a type family of one parameter is implemented by the following code:
so that we could define a type
RealVectorSpace
usingand write
to create an object of type
RealVectorSpace
. The above code works by creating a meta-class (i.e. subclass ofstandard-class
)DepSum
, which represents the type of all dependent sums and whose instances are dependent sums of specific families. Type safety is enforced by hooking into calls like(make-instance (depsum #'RealVectorSpaceOver) ...)
via(defmethod make-instance :before ((sum-class DepSum ...
. Unfortunately, it seems we have to rely onassert
for this type check (I wasn't able to figure out how to make it work withdeclare
). Lastly, the code(defmethod sb-mop:validate-superclass ...
is implementation-dependent (for SBCL in this case) and necessary in order to be able to instantiate instances ofDepSum
like(depsum #'RealVectorSpaceOver)
.Why is this only a partial answer? Because I have not made the vector space axioms part of the type of
RealVectorSpaceOver
(orRealVectorSpace
). Indeed, such a thing would require to give an actual proof of these axioms as part of the datum in a call to(make-instance (RealVectorSpaceOver X) ...
. Such a thing is certainly possible in fancy languages like Coq, but seems totally out-of-reach in that old, yet lovable mess that is Common Lisp.