在你开始阅读之前:这个问题不是关于理解 monad,而是关于识别 Java 类型系统的限制,它阻止了Monad
接口的声明。
在我努力理解 monad 的过程中,我阅读了 Eric Lippert 的这个SO-answer 关于一个关于 monad 的简单解释的问题。在那里,他还列出了可以在 monad 上执行的操作:
- 有一种方法可以获取未放大类型的值并将其转换为放大类型的值。
- 有一种方法可以将未放大类型的操作转换为放大类型的操作,该操作遵循前面提到的函数组合规则
- 通常有一种方法可以将未放大的类型从放大的类型中恢复出来。(最后一点对于 monad 不是绝对必要的,但经常存在这样的操作。)
在阅读了有关 monad 的更多信息后,我将第一个操作确定为return
函数,将第二个操作确定为bind
函数。我找不到第三个操作的常用名称,所以我将它称为unbox
函数。
为了更好地理解 monad,我继续尝试Monad
在 Java 中声明一个泛型接口。为此,我首先查看了上面三个函数的签名。对于 Monad M
,它看起来像这样:
return :: T1 -> M<T1>
bind :: M<T1> -> (T1 -> M<T2>) -> M<T2>
unbox :: M<T1> -> T1
该return
函数不在 的实例上执行M
,因此它不属于Monad
接口。相反,它将被实现为构造函数或工厂方法。
同样,现在,我从接口声明中省略了该unbox
函数,因为它不是必需的。对于接口的不同实现,这个函数会有不同的实现。
因此,Monad
接口只包含bind
功能。
让我们尝试声明接口:
public interface Monad {
Monad bind();
}
有两个缺陷:
- 该
bind
函数应该返回具体的实现,但是它只返回接口类型。这是一个问题,因为我们在具体子类型上声明了拆箱操作。我将此称为问题 1。 - 该
bind
函数应该检索一个函数作为参数。我们稍后会解决这个问题。
在接口声明中使用具体类型
这解决了问题 1:如果我对 monad 的理解是正确的,那么该bind
函数总是返回一个与调用它的 monad 具有相同具体类型的新 monad。因此,如果我有一个Monad
名为 的接口的实现M
,那么M.bind
将返回另一个M
但不返回Monad
. 我可以使用泛型来实现它:
public interface Monad<M extends Monad<M>> {
M bind();
}
public class MonadImpl<M extends MonadImpl<M>> implements Monad<M> {
@Override
public M bind() { /* do stuff and return an instance of M */ }
}
起初,这似乎可行,但至少有两个缺陷:
一旦实现类不提供自己而是提供
Monad
接口的另一个实现作为类型参数,这就会崩溃M
,因为这样该bind
方法将返回错误的类型。例如public class FaultyMonad<M extends MonadImpl<M>> implements Monad<M> { ... }
将返回一个实例
MonadImpl
,它应该返回一个FaultyMonad
. 但是,我们可以在文档中指定此限制,并将此类实现视为程序员错误。第二个缺陷更难解决。我将其称为问题 2:当我尝试实例化类时,
MonadImpl
我需要提供M
. 让我们试试这个:new MonadImpl<MonadImpl<MonadImpl<MonadImpl<MonadImpl< ... >>>>>()
要获得有效的类型声明,这必须无限进行。这是另一个尝试:
public static <M extends MonadImpl<M>> MonadImpl<M> create() { return new MonadImpl<M>(); }
虽然这似乎可行,但我们只是将问题推迟到被调用方。这是该功能对我有用的唯一用法:
public void createAndUseMonad() { MonadImpl<?> monad = create(); // use monad }
这基本上归结为
MonadImpl<?> monad = new MonadImpl<>();
但这显然不是我们想要的。
在自己的声明中使用带有移位类型参数的类型
现在,让我们将函数参数添加到bind
函数中: 如上所述,bind
函数的签名如下所示:T1 -> M<T2>
. 在 Java 中,这是类型Function<T1, M<T2>>
。这是第一次尝试用参数声明接口:
public interface Monad<T1, M extends Monad<?, ?>> {
M bind(Function<T1, M> function);
}
我们必须将类型T1
作为泛型类型参数添加到接口声明中,以便我们可以在函数签名中使用它。第一个?
是T1
返回的 monad 类型M
。要将其替换为T2
,我们必须将T2
其自身添加为泛型类型参数:
public interface Monad<T1, M extends Monad<T2, ?, ?>,
T2> {
M bind(Function<T1, M> function);
}
现在,我们遇到了另一个问题。我们在接口中添加了第三个类型参数Monad
,所以我们必须添加一个新?
的来使用它。我们将暂时忽略新?
的,而首先研究现在?
。它是M
返回的 monad 类型M
。让我们尝试?
通过重命名并引入另一个M
来删除它:M1
M2
public interface Monad<T1, M1 extends Monad<T2, M2, ?, ?>,
T2, M2 extends Monad< ?, ?, ?, ?>> {
M1 bind(Function<T1, M1> function);
}
引入另一个T3
结果:
public interface Monad<T1, M1 extends Monad<T2, M2, T3, ?, ?>,
T2, M2 extends Monad<T3, ?, ?, ?, ?>,
T3> {
M1 bind(Function<T1, M1> function);
}
并引入另一个M3
结果:
public interface Monad<T1, M1 extends Monad<T2, M2, T3, M3, ?, ?>,
T2, M2 extends Monad<T3, M3, ?, ?, ?, ?>,
T3, M3 extends Monad< ?, ?, ?, ?, ?, ?>> {
M1 bind(Function<T1, M1> function);
}
我们看到,如果我们尝试解决所有问题,这将永远持续下去?
。这是问题 3。
总结一下
我们发现了三个问题:
- 在抽象类型的声明中使用具体类型。
- 实例化一个接收自身作为泛型类型参数的类型。
- 声明一个在其声明中使用自身的类型,并带有移位的类型参数。
问题是:Java 类型系统中缺少什么特性?由于有些语言可以使用 monad,因此这些语言必须以某种方式声明Monad
类型。这些其他语言如何声明Monad
类型?我无法找到有关此的信息。我只找到有关具体单子声明的信息,例如Maybe
单子。
我错过了什么吗?我可以用 Java 类型系统正确解决这些问题之一吗?如果我不能用 Java 类型系统解决问题 2,那么 Java 没有警告我关于不可实例化类型声明的原因是什么?
如前所述,这个问题与理解单子无关。如果我对 monads 的理解是错误的,你可能会给出提示,但不要试图给出解释。如果我对单子的理解是错误的,那么所描述的问题仍然存在。
这个问题也不是关于是否可以Monad
在 Java 中声明接口。这个问题已经得到了 Eric Lippert 在上面链接的 SO-answer 中的回答:不是。这个问题是关于阻止我这样做的究竟是什么限制。Eric Lippert 将此称为高级类型,但我无法理解它们。
大多数 OOP 语言没有足够丰富的类型系统来直接表示 monad 模式本身;您需要一个支持比泛型类型更高的类型的类型系统。所以我不会尝试那样做。相反,我将实现表示每个 monad 的泛型类型,并实现表示您需要的三个操作的方法:将值转换为放大值,将放大值转换为值,以及将未放大值上的函数转换为放大的值。