Guarded methods in OCaml

Guarded methods in OCaml

OCaml 中的守卫方法 (Guarded methods)

Guarded methods allow attaching constraints to the receiver (self) only for certain methods, thus allowing these methods to be called only if the receiver satisfies these constraints (these guards). OCaml does not syntactically allow defining this kind of method directly. In this note, we will see how to encode them using a type equality witness.

守卫方法允许仅针对特定方法对接收者(self)施加约束,从而使得这些方法仅在接收者满足这些约束(即守卫)时才能被调用。OCaml 在语法上并不直接支持定义此类方法。在本篇笔记中,我们将探讨如何利用类型相等证明(type equality witness)来编码实现它们。

Problem presentation

问题陈述

When a language (where type checking is done before the program runs, like in Java or OCaml) introduces parametric polymorphism (Java’s generics), it’s sometimes possible to constrain type variables. For example: class MyClass<T extends S> { ... }. We make MyClass generic by assuming that the type variable T is a subtype of S. The problem is that the constraint applies to the entire class. Yet sometimes, we’d like to have constraints apply only to certain methods.

当一门语言(在程序运行前进行类型检查,如 Java 或 OCaml)引入参数化多态(Java 的泛型)时,有时可以对类型变量进行约束。例如:class MyClass<T extends S> { ... }。我们通过假设类型变量 T 是 S 的子类型来使 MyClass 泛型化。问题在于,该约束会应用于整个类。然而有时,我们希望约束仅应用于特定的方法。

For example, let’s say we have a class MyList describing a list: class MyList<A> extends ArrayList<A> { public int length() { return this.size(); } } How can we define a flatten method that, for a list like [[1, 2, 3], [4, 5]], would produce the list [1, 2, 3, 4, 5]? If we place the constraint at the class level, we force our list to be “always a list of lists,” which is very limiting. To implement such a method, we have three theoretical approaches available.

例如,假设我们有一个描述列表的类 MyList: class MyList<A> extends ArrayList<A> { public int length() { return this.size(); } } 我们该如何定义一个 flatten 方法,使得对于像 [[1, 2, 3], [4, 5]] 这样的列表,能生成 [1, 2, 3, 4, 5] 呢?如果我们把约束放在类级别,就会强迫我们的列表“永远是列表的列表”,这限制很大。为了实现这样的方法,我们有三种理论上的方法。

Moving the method outside the class

将方法移出类

The first solution is the most obvious: simply “cheat” by moving the method outside the class body (for example, into the static context or a companion object): class MyList<A> extends ArrayList<A> { public static <A> MyList<A> flatten(MyList<MyList<A>> list) { // Flatten's implementation } public int length() { return this.size(); } } This approach works and doesn’t require any special ceremony. However, it forces the developer to keep track of which methods are in the class body and which are in the static context. Moreover, it breaks the systematic approach of sending messages to an instance (often presented as one of the key arguments in favor of object-oriented programming).

第一种解决方案最显而易见:简单地“作弊”,将方法移出类体(例如,放入静态上下文或伴生对象中): class MyList<A> extends ArrayList<A> { public static <A> MyList<A> flatten(MyList<MyList<A>> list) { // Flatten's implementation } public int length() { return this.size(); } } 这种方法有效且不需要任何特殊处理。然而,它迫使开发者必须时刻留意哪些方法在类体中,哪些在静态上下文中。此外,它破坏了向实例发送消息的系统化方法(这通常被视为支持面向对象编程的关键论点之一)。

Extension methods

扩展方法

Kotlin (and others, like C#) offer extension methods which, in addition to allowing the extension of an already existing class (which can be very useful for adding behavior to the String class, which is final in Java), also provide more flexibility in defining the receiver. For example, we could write flatten like this (in Kotlin): class MyList<A> : ArrayList<A> { ... } fun <A> MyList<MyList<A>>.flatten() = ... Even though this solution seems nearly perfect, it still requires the method to be defined outside the class, which might potentially mean having to make certain members of the class public in order to be accessible from an extension (looks like potentials leaky abstractions). However, it still preserves the systematic message-sending approach while allowing for more fine-grained qualification of the receiver.

Kotlin(以及其他语言,如 C#)提供了扩展方法。除了允许扩展现有的类(这对于为 Java 中不可继承的 String 类添加行为非常有用)之外,它们在定义接收者时也提供了更大的灵活性。例如,我们可以这样编写 flatten(在 Kotlin 中): class MyList<A> : ArrayList<A> { ... } fun <A> MyList<MyList<A>>.flatten() = ... 尽管这个方案看起来近乎完美,但它仍然要求方法定义在类之外,这可能意味着必须将类的某些成员公开,以便从扩展中访问(这看起来像是潜在的抽象泄漏)。不过,它确实保留了系统化的消息发送方式,同时允许对接收者进行更细粒度的限定。

Guarded methods

守卫方法

The final approach is probably the most ideological, as it keeps the method definition within the class. It doesn’t force any escaped abstractions. This is the idea of guarded methods, the ability to add constraints on the generic parameter at the method definition level. In an imaginary syntax: class MyList<A> : ArrayList<A>() { fun length() = size fun <B> MyList<MyList<B>>.flatten() = // Implémentation de flatten } Even though this doesn’t seem significantly different from classic extension methods, it addresses all the issues raised earlier: we can characterize the receiver more precisely than in a normal method; we don’t break the regular message sending; we still benefit from available members (so we don’t escape representations).

最后一种方法可能是最符合理想的,因为它将方法定义保留在类内部。它不会强制任何抽象逃逸。这就是守卫方法的理念:能够在方法定义级别对泛型参数添加约束。在一种假想的语法中: class MyList<A> : ArrayList<A>() { fun length() = size fun <B> MyList<MyList<B>>.flatten() = // Implémentation de flatten } 尽管这看起来与经典的扩展方法没有太大区别,但它解决了之前提出的所有问题:我们可以比普通方法更精确地描述接收者;我们没有破坏常规的消息发送;我们仍然可以利用现有的成员(因此不会导致表示逃逸)。

Although guarded methods seem necessary, unfortunately, I don’t know of any mainstream languages that allow their definition. That’s very sad. Fortunately, in OCaml, it is possible to encode them.

虽然守卫方法看起来很有必要,但不幸的是,我不知道有任何主流语言允许定义它们。这非常令人遗憾。幸运的是,在 OCaml 中,是有可能编码实现它们的。

OOP/FP symmetry: theory and practice

OOP/FP 对称性:理论与实践

Since guarded methods are quite rare in popular programming languages, I discovered their existence fairly recently while reading the slides from the presentation “The Object-Oriented/Functional-Programming symmetry: theory and practice” by Gabriel Scherer. I recommend this presentation, which showcases a symmetry between the tools of statically typed functional programming and object-oriented programming.

由于守卫方法在流行编程语言中非常罕见,我是在最近阅读 Gabriel Scherer 的演讲幻灯片《面向对象/函数式编程对称性:理论与实践》时才发现它们的。我推荐这个演讲,它展示了静态类型函数式编程工具与面向对象编程工具之间的对称性。

Unfortunately not covered during the presentation, an entire section on guarded methods is included in the slides. The original example offers a symmetrical observation between the implementation of the flatten function in a classic functional style and the implementation of a flatten method if we were in the object-oriented world, posing exactly the problem introduced in this note. The question is: what type should flatten have? class type ['a] olist = object method length : int method concat : 'a olist -> 'a olist method flatten : ??? end He therefore proposes this syntax, which implies a guard on the flatten method: method flatten : 'b olist with 'a = 'b olist This syntax allows describing a guarded method and could be generalized.

遗憾的是,演讲中并未涵盖这一部分,但幻灯片中包含了一个关于守卫方法的完整章节。原始示例展示了经典函数式风格中 flatten 函数的实现与面向对象世界中 flatten 方法实现之间的对称观察,这恰好提出了本笔记中介绍的问题。问题是:flatten 应该是什么类型? class type ['a] olist = object method length : int method concat : 'a olist -> 'a olist method flatten : ??? end 因此,他提出了这种语法,暗示了对 flatten 方法的守卫: method flatten : 'b olist with 'a = 'b olist 这种语法允许描述一个守卫方法,并且可以被推广。