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.

4

2 回答 2

2

这在 Common Lisp 中是不容易实现的。

在 Common Lisp 中,操作(泛型函数)与类型(类)是分开的,即它们不为类型“拥有”。它们的调度是在运行时完成的,也可以在运行时添加、修改和删除方法。

通常,缺少方法的错误仅在运行时发出信号。编译器无法知道泛型函数是否被“很好地”使用。

Common Lisp 中惯用的方式是使用泛型函数并描述其需求,或者换句话说,Common Lisp 中最接近接口的是一组泛型函数和一个标记 mixin 类。但大多数情况下,只指定一个协议,以及它对其他协议的依赖关系。例如,参见CLIM 规范

至于类型类,它是一个关键特性,它不仅使语言保持完全类型安全,而且使其在这方面非常可扩展。否则,要么类型系统过于严格,要么缺乏表现力会导致类型不安全的情况,至少从编译器的角度来看是这样。请注意,Haskell 在运行时不保留或不必保留对象类型,它在编译时进行每个类型推断,这与惯用的 Common Lisp 形成鲜明对比。

要在运行时拥有类似于 Common Lisp 中的类型类的东西,您有几个选择

如果您选择使用其规则支持类型类,我建议您使用元对象协议

  • 定义一个新的通用函数元类(即继承自的standard-generic-function

  • 专门compute-applicable-methods-using-classes返回 false 作为第二个值,因为 Common Lisp 中的类仅由它们的名称表示,它们不是“可参数化的”或“可约束的”

  • 专门compute-applicable-methods检查参数的元类的类型或规则,相应地调度并可能记忆结果

如果您选择仅具有可参数化的类型(例如模板、泛型),现有的选项是Lisp 接口库,您可以在其中传递一个使用协议实现特定策略的对象。但是,我主要将其视为策略模式的实现,或显式的控制反转,而不是实际的可参数化类型。

对于实际的可参数化类型,您可以定义抽象的未参数化类,从中您可以使用有趣的名称来实习具体实例,例如lib1:collection<lib2:object>,包中collection定义的抽象类在哪里,而实际上是名称的一部分,就像具体类一样。lib1lib2:object

最后一种方法的好处是您可以在 CLOS 的任何地方使用这些类和名称。

主要缺点是您仍然必须生成具体的类,因此您可能会拥有自己的defmethod-like 宏,该宏将扩展为使用find-class-like 函数的代码,该函数知道如何执行此操作。因此破坏了我刚刚提到的大部分好处,否则您应该遵循在自己的库中定义每个具体类的规则,然后再将它们用作专家。

另一个缺点是,如果没有进一步的重要管道,这太静态了,不是真正的通用,因为它没有考虑到 eglib1:collection<lib2:subobject>可能是子类,lib1:collection<lib2:object>反之亦然。一般来说,它没有考虑计算机科学中所谓的协变和逆变

但是你可以实现它:lib:collection<in out>可以用一个逆变参数和一个协变参数来表示抽象类。如果可能的话,困难的部分将是生成和维护具体类之间的关系。

一般来说,编译时方法在 Lisp 实现级别更合适。这样的 Lisp 很可能不是 Common Lisp。您可以做的一件事是为 Haskell 提供类似 Lisp 的语法。它的完整元循环是在宏扩展级别使其完全类型安全,例如为宏本身生成编译时类型错误,而不仅仅是为它们生成的代码。


编辑:在您的问题编辑之后,我必须说,只要方法中有类型类专门化器,它就compute-applicable-methods-using-classes 必须作为第二个值返回。否则nil你可以。call-next-method

这与适用方法中存在类型类专门化器不同。请记住,CLOS 对类型类一无所知,因此通过返回c-a-m-u-c具有真正第二个值的内容,您是在说仅给定类就可以记忆(缓存)。

您必须真正专注compute-applicable-methods于正确的类型类调度。如果有机会进行记忆(缓存),您必须自己在此处进行。

于 2015-11-24T01:07:29.633 回答
1

我相信您需要覆盖compute-applicable-methods和/或compute-applicable-methods-using-classes计算实现通用函数调用所需的方法列表。然后,您可能需要覆盖compute-effective-method将列表和其他一些内容组合到一个函数中,该函数可以在运行时调用以执行方法调用。

我真的推荐阅读The Art of the Metaobject Protocol(如前所述),其中详细介绍了这一点。然而,总而言之,假设您foo在某些类上定义了一个方法(这些类不需要以任何方式相关)。评估 lisp 代码(foo obj)调用返回的函数,该函数compute-effective-method检查参数以确定要调用的方法,然后调用它们。的目的compute-effective-method是通过将类型测试编译成案例语句或其他条件来尽可能多地消除运行时成本。因此,Lisp 运行时不必在每次进行方法调用时查询所有方法的列表,而只需在添加、删除或更改方法实现时查询。通常所有这些都在加载时完成一次,然后保存到您的 lisp 图像中以获得更好的性能,同时仍然允许您在不停止系统的情况下更改这些内容。

于 2015-11-22T21:42:48.837 回答