【发布时间】:2016-03-11 23:36:48
【问题描述】:
开始阅读之前:这个问题不是关于理解 monad,而是关于识别 Java 类型系统的限制,该限制阻止了 Monad 接口的声明。
为了理解 monad,我阅读了 Eric Lippert 的this SO-answer,该问题询问了对 monad 的简单解释。在那里,他还列出了可以在 monad 上执行的操作:
- 有一种方法可以获取未放大类型的值并将其转换为放大类型的值。
- 有一种方法可以将未放大类型的操作转换为放大类型的操作,该操作遵循前面提到的函数组合规则
- 通常有一种方法可以将未放大的类型从放大的类型中恢复出来。 (最后一点对于 monad 并不是绝对必要的,但这种操作经常存在。)
在阅读了有关 monad 的更多信息后,我将第一个操作确定为 return 函数,将第二个操作确定为 bind 函数。我找不到第三个操作的常用名称,所以我将其称为unbox 函数。
为了更好地理解 monad,我继续尝试在 Java 中声明一个通用的Monad 接口。为此,我首先查看了上面三个函数的签名。对于 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。所以,如果我有一个名为M 的Monad 接口的实现,那么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作为泛型类型参数添加到接口声明中,以便我们可以在函数签名中使用它。第一个? 是M 类型返回的monad 的T1。要将其替换为T2,我们必须将T2 本身添加为泛型类型参数:
public interface Monad<T1, M extends Monad<T2, ?, ?>,
T2> {
M bind(Function<T1, M> function);
}
现在,我们遇到了另一个问题。我们在Monad 接口中添加了第三个类型参数,因此我们必须添加一个新的? 来使用它。我们将暂时忽略新的? 来调查现在的第一个?。它是返回的 M 类型的单子的 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 类型?我无法找到有关此的信息。我只找到有关具体 monad 声明的信息,例如 Maybe monad。
我错过了什么吗?我可以用 Java 类型系统正确解决这些问题之一吗?如果我不能用 Java 类型系统解决问题 2,那么 Java 没有警告我关于不可实例化类型声明的原因是什么?
如前所述,这个问题不是关于理解单子的。如果我对 monads 的理解是错误的,你可能会给出提示,但不要试图给出解释。如果我对单子的理解是错误的,那么所描述的问题仍然存在。
这个问题也不是关于是否可以在Java中声明Monad接口。这个问题已经得到了 Eric Lippert 在上面链接的 SO-answer 中的回答:不是。这个问题是关于阻止我这样做的究竟是什么限制。 Eric Lippert 将其称为高级类型,但我无法理解它们。
大多数 OOP 语言没有足够丰富的类型系统来直接表示 monad 模式本身;您需要一个支持比泛型类型更高的类型的类型系统。所以我不会尝试那样做。相反,我将实现表示每个 monad 的泛型类型,并实现表示您需要的三个操作的方法:将值转换为放大值,将放大值转换为值,以及将未放大值上的函数转换为放大的值。
【问题讨论】:
-
更高的kinded类型。
-
@BrianGoetz:早上好,Brian,感谢您的评论。请随时纠正我在 Java 泛型等方面所犯的任何错误!
标签: java generics monads type-systems higher-order-types