2

I'm trying to simulate something akin to Haskell's typeclasses with Common Lisp's CLOS. That is, I'd like to be able to dispatch a method on an object's "typeclasses" instead of its superclasses.

I have a metaclass defined for classes which have and implement typeclasses(which are just other classes). Those classes(those that implement typeclasses) have a slot containing the list of the typeclasses they implement.
I'd like to be able to define methods for a typeclass, and then be able to dispatch that method on objects whose class implement that typeclass. And I'd like to be able to add and remove typeclasses dynamically.

I figure I could probably do this by changing the method dispatch algorithm, though that doesn't seem too simple.

Anybody is comfortable enough with CLOS and the MOP to give me some suggestions?

Thanks.

Edit: My question might be specified as, how do I implement compute-applicable-methods-using-classes and compute-applicable-methods for a "custom" generic-function class such that if some of the specializers of a generic function method are typeclasses(classes whose metaclass is the 'typeclass' class), then the corresponding argument's class must implement the typeclass(which simply means having the typeclass stored in a slot of the argument's class) for the method to be applicable?
From what I understand from documentation, when a generic function is called, compute-discriminating-functionis first called, which will first attempt to obtain applicable methods through compute-applicable-methods-using-classes, and if unsuccessful, will try the same with compute-applicable-methods.
While my definition of compute-applicable-methods-using-classes seems to work, the generic function fails to dispatch an applicable function. So the problem must be in compute-discriminating-function or compute-effective-method.

See code.

Jongware
  • 22,200
  • 8
  • 54
  • 100
Charles Langlois
  • 4,198
  • 4
  • 16
  • 25
  • 1
    Similar question: http://stackoverflow.com/questions/30637469/typeclasses-in-common-lisp – Rainer Joswig Nov 19 '15 at 06:29
  • 1
    I'm not confortable enough with that topics, but I think that you should read the art of the meta object protocol. https://mitpress.mit.edu/books/art-metaobject-protocol – anquegi Nov 19 '15 at 13:30
  • 1
    You should have a look at Faré's Lisp Interface Library: https://common-lisp.net/~frideau/lil-ilc2012/lil-ilc2012.html. From my understanding, this is what typeclasses could look like in a dynamically typed language. – coredump Nov 19 '15 at 15:45
  • 1
    Can you explain a bit about what normal generic functions *don't* do for you here? I understand that CLOS classes aren't the same as Haskell's typeclasses, but since generic functions don't "belong" to classes in CLOS, it seems like a typeclass would just be a collection of generic functions, and making a class an instance of that typeclass would just mean defining the appropriate methods on those generic functions. What breaks down in that approach? Is it just that you want some assurance that all the appropriate methods are defined? – Joshua Taylor Nov 19 '15 at 18:18
  • 1
    E.g., what *wouldn't* work with something like https://gist.github.com/tayloj/1f5cea36ea32201d2e3c? – Joshua Taylor Nov 19 '15 at 18:29
  • @anquegi : I'm planning on it. – Charles Langlois Nov 20 '15 at 05:26
  • 1
    @JoshuaTaylor: Making sure all necessary methods of a typeclass are defined is one issue. Having default definitions for members of a typeclass is another. My idea of CLOS' "typeclasses" is effectively that of a collection of generics(and default method definitions). A "typeclass" class has a slot containing a list of those generics. I can also imagine some kind of typeclass inheritance, or some other relations between typeclasses(e.g. as in Haskell, where an instance of Monad must also be an instance of Functor and Applicative). – Charles Langlois Nov 20 '15 at 05:35
  • I've updated my answer after your edit. Can you provide a test case where you use this specialization of `compute-applicable-methods-using-classes`? Is this all the code you need to suport type class specialization? – acelent Nov 29 '15 at 00:29
  • @PauloMadeira To be clear, I had to specialize `compute-applicable-methods` to make it work. So my specialization of `compute-applicable-methods-using-classes` is not used, I expect. Though I understand why I shouldn't use it anyway. – Charles Langlois Nov 29 '15 at 22:28
  • Also, you shouldn't mark a question as solved in the title, you should accept an answer, even if it's your own answer. – acelent Nov 30 '15 at 13:24
  • @PauloMadeira Sorry, that's done. – Charles Langlois Dec 01 '15 at 19:41

2 Answers2

2

This is not easily achievable in Common Lisp.

In Common Lisp, operations (generic functions) are separate from types (classes), i.e. they're not "owned" by types. Their dispatch is done at runtime, with the possibility of adding, modifying and removing methods at runtime as well.

Usually, errors from missing methods are signaled only at runtime. The compiler has no way to know if a generic function is being "well" used or not.

The idiomatic way in Common Lisp is to use generic functions and describe its requirements, or in other words, the closest to an interface in Common Lisp is a set of generic functions and a marker mixin class. But most usually, only a protocol is specified, and its dependencies on other protocols. See, for instance, the CLIM specification.

As for type classes, it's a key feature that keeps the language not only fully type-safe, but also makes it very extensible in that aspect. Otherwise, either the type system would be too strict, or the lack of expressiveness would lead to type-unsafe situations, at least from the compiler's point of view. Note that Haskell doesn't keep, or doesn't have to keep, object types at runtime, it takes every type inference at compile-time, much in contrast with idiomatic Common Lisp.

To have something similar to type classes in Common Lisp at runtime, you have a few choices

Should you choose to support type classes with its rules, I suggest you use the meta-object protocol:

  • Define a new generic function meta-class (i.e. one which inherits from standard-generic-function)

  • Specialize compute-applicable-methods-using-classes to return false as a second value, because classes in Common Lisp are represented solely by their name, they're not "parameterizable" or "constrainable"

  • Specialize compute-applicable-methods to inspect the argument's meta-classes for types or rules, dispatch accordingly and possibly memoize results

Should you choose to only have parameterizable types (e.g. templates, generics), an existing option is the Lisp Interface Library, where you pass around an object that implements a particular strategy using a protocol. However, I see this mostly as an implementation of the strategy pattern, or an explicit inversion of control, rather than actual parameterizable types.

For actual parameterizable types, you could define abstract unparameterized classes from which you'd intern concrete instances with funny names, e.g. lib1:collection<lib2:object>, where collection is the abstract class defined in the lib1 package, and the lib2:object is actually part of the name as is for a concrete class.

The benefit of this last approach is that you could use these classes and names anywhere in CLOS.

The main disadvantage is that you must still generate concrete classes, so you'd probably have your own defmethod-like macro that would expand into code that uses a find-class-like function which knows how to do this. Thus breaking a significant part of the benefit I just mentioned, or otherwise you should follow the discipline of defining every concrete class in your own library before using them as specializers.

Another disadvantage is that without further non-trivial plumbing, this is too static, not really generic, as it doesn't take into account that e.g. lib1:collection<lib2:subobject> could be a subclass of lib1:collection<lib2:object> or vice-versa. Generically, it doesn't take into account what is known in computer science as covariance and contravariance.

But you could implement it: lib:collection<in out> could represent the abstract class with one contravariant argument and one covariant argument. The hard part would be generating and maintaining the relationships between concrete classes, if at all possible.

In general, a compile-time approach would be more appropriate at the Lisp implementation level. Such Lisp would most probably not be a Common Lisp. One thing you could do is to have a Lisp-like syntax for Haskell. The full meta-circle of it would be to make it totally type-safe at the macro-expansion level, e.g. generating compile-time type errors for macros themselves instead of only for the code they generate.


EDIT: After your question's edit, I must say that compute-applicable-methods-using-classes must return nil as a second value whenever there is a type class specializer in a method. You can call-next-method otherwise.

This is different than there being a type class specializer in an applicable method. Remember that CLOS doesn't know anything about type classes, so by returning something from c-a-m-u-c with a true second value, you're saying it's OK to memoize (cache) given the class alone.

You must really specialize compute-applicable-methods for proper type class dispatching. If there is opportunity for memoization (caching), you must do so yourself here.

acelent
  • 7,965
  • 21
  • 39
  • 1
    Thanks for these suggestions. What I mainly want to implement is the inheritance of default method definitions for a typeclass, runtime typeclass membership and satisfaction check(all required methods of a typeclass are implemented by the class), and typeclass inheritance and relations(e.g. a typeclass automatically implements default methods for another typeclass). So I understand I'll have to implement my own `compute-applicable-methods`, which will be the main difficulty of this endeavor. – Charles Langlois Nov 26 '15 at 06:12
  • Okay, I understand. Although that doesn't explain why it didn't used the return values from c-a-m-u-c. Thanks. – Charles Langlois Nov 29 '15 at 22:25
1

I believe you'll need to override compute-applicable-methods and/or compute-applicable-methods-using-classes which compute the list of methods that will be needed to implement a generic function call. You'll then likely need to override compute-effective-method which combines that list and a few other things into a function which can be called at runtime to perform the method call.

I really recommend reading The Art of the Metaobject Protocol (as was already mentioned) which goes into great detail about this. To summarize, however, assume you have a method foo defined on some classes (the classes need not be related in any way). Evaluating the lisp code (foo obj) calls the function returned by compute-effective-method which examines the arguments in order to determine which methods to call, and then calls them. The purpose of compute-effective-method is to eliminate as much of the run-time cost of this as is possible, by compiling the type tests into a case statement or other conditional. The Lisp runtime thus does not have to query for the list of all methods each time you make a method call, but only when you add, remove or change a method implementation. Usually all of that is done once at load time and then saved into your lisp image for even better performance while still allowing you to change these things without stopping the system.

db48x
  • 3,108
  • 24
  • 16
  • Okay, thanks. I'm planning on getting myself a copy of AMOP. – Charles Langlois Nov 23 '15 at 03:03
  • The (portable) optimizing approach is to memoize at the `compute-applicable-methods` level. Method combinations have a completely different meaning, e.g. you could still have `(defmethod first-result :or ((a equatable)) ...)`, so `first-result` could still be a generic function with a specialized meta-class and `compute-applicable-methods`. – acelent Nov 24 '15 at 01:17