OCaml 中的守卫方法
Guarded Methods in OCaml (2025)

原始链接: https://xvw.lol/en/articles/oop-refl.html

“受限方法”(Guarded methods)允许为类中的单个方法附加特定的类型约束,而不是限制整个类。虽然常见的面向对象语言缺乏对此的原生语法支持,但 OCaml 允许我们利用**类型相等见证**(type equality witnesses)来实现这一机制。 通常,参数化多态会约束整个类,这对于某些操作来说过于死板(例如,列表的 `flatten` 方法要求列表内必须包含列表,而 `sum` 方法则要求其包含整数)。常见的替代方案——如将方法移至静态上下文或使用扩展方法——要么破坏了面向对象的消息传递范式,要么导致了抽象的泄露。 在 OCaml 中,我们可以通过要求传入一个 `('a, 'b) eq` 类型的附加参数来实现受限方法。利用 GADT 中的 `Refl` 构造器,开发者可以提供一个运行时证明,表明接收者的类型满足所需的约束。这有效地限制了方法的访问;尝试在列表的列表上调用 `sum` 将导致编译错误,因为无法满足所需的相等性见证。虽然这种方法比原生语言支持更为冗长,但它为在面向对象框架内表达复杂约束提供了一种强大且类型安全的方式。

这篇 Hacker News 的讨论围绕着一篇关于在 OCaml 中引入“受限方法”(guarded methods)的文章展开——即仅当类型参数满足特定约束时(例如 `Array.contains` 要求类型必须实现 `Equatable`)才可调用类方法。 评论者讨论了此类特性在各种编程语言中的实际必要性及实现方式: * **语言设计**:一位用户提到他们目前正在自己的语言 `zena` 中尝试类似的约束,并将其与 TypeScript 的 `this` 类型进行了对比。 * **现有实现**:参与者指出,“受限方法”在其他语言中早已存在。C++ 通过 Concepts 或 SFINAE 实现,而 Rust 则使用条件 `impl` 代码块。 * **方法论**:关于作者偏好实例方法而非静态函数的做法引发了争论。批评者质疑,为了保持面向对象的纯粹性而强行将方法置于类内部,是否比单纯使用静态方法或扩展方法更易导致混乱或产生“意大利面条式代码”。 * **封装性**:讨论强调,尽管 Rust 通过条件实现解决了这一问题,但作者希望在保持面向对象范式的同时,将成员定义严格限制在类边界内的这一特定需求,仍属于大多数主流语言难以满足的小众设计要求。
相关文章

原文

Guarded methods make it possible to attach constraints to the receiver (self) only for certain methods, meaning these methods can only be called if the receiver satisfies those constraints (these guards). OCaml does not, syntactically, allow defining this kind of method directly. In this note, we’ll look at how to encode them using a type equality witness.

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.

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. Even though this symmetry has been observed and studied many times, the presentation is comprehensive and accessible (and relatively unbiased, discussing the pros and cons of both approaches). Unfortunately not covered during the presentation (time is often the enemy of a presenter), 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:

type 'a list = ...
let rec length : 'a list -> int = ...
let rec concat : 'a list -> 'a list -> 'a list = ...

let rec flatten : 'a list list -> 'a list = function
  | [] -> []
  | x::xs -> x @ flatten xs

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 like this: method method_name : return_type with generic_type = other_type. Similar to substitutions in modules, we could specify constraints on multiple generics using and. For example: method foo : string with 'a = string and b = int for a class parameterized by two types: class ['a, 'b] t.

Additionally, this syntax would also allow defining specific behaviors elegantly. For example, for our olist type, we could provide a sum method if the elements of the list are integers:

class type ['a] olist = object
  method length : int
  method concat : 'a olist -> 'a olist
  method flatten : 'b olist with 'a = 'b olist
  method sum : int with 'a = int
end

All of this sounds extraordinary, but unfortunately, this syntax is not available in OCaml. That’s annoying! Don’t worry, it is possible to encode it using a few small tools.

Florian Angeletti, also known as Octachron. (A fun little note: octachron is the name of a MIDI drum sequencer, so when I searched his nickname on Google, the suggestions immediately included octachron ocaml).

Our goal is to allow adding a constraint to certain methods so that they are only accessible if the receiver’s type satisfies it. Without modifying the language syntax, modeling a constraint can consist of providing an additional parameter that enforces it. In other words, we want to provide evidence.

generalized algebraic data types in the language, there is a fairly straightforward way to define a type equality witness:

type (_, _) eq =
  | Refl : ('a, 'a) eq

The eq type, which has only one constructor: Refl, allows representing type equalities not known by the type-checker. Since we can only construct Refl values that associate two equal types, instantiating Refl within a scope guarantees that those types are equivalent. For example:

type other_int = int
let _ : (int, other_int) eq = Refl

This example is somewhat artificial because here the compiler knows perfectly well that int = other_int. However, there are cases where the compiler cannot know this. For example, when data is provided at runtime, where it makes perfect sense that the type-checker has no information about a type, or when the type’s representation is hidden by abstraction.

The goal of this note is not to delve into eq, so let’s just keep in mind that if we can construct a Refl value, we have a guarantee that two syntactically different types are actually equal.

Nicolas Rinaudo, that the language Scala uses a similar encoding, but where the type equality witness is provided implicitly, thus lightening the call and not forcing the user to manually provide Refl.

Even though the encoding is somewhat heavy, and one could imagine native language support to simplify the definition of guarded methods, explicitly manipulating a type equality witness allows us to encode them. Is it useful? Since OOP programming is rarely encouraged in OCaml, probably not, but it was still fun to present a concrete and practical use case for equality witnesses!

联系我们 contact @ memedata.com