为什么Monad接口不能在Java中声明?

14 浏览
0 Comments

为什么Monad接口不能在Java中声明?

在开始阅读之前:这个问题不是关于理解单子的问题,而是关于识别Java类型系统的限制,这些限制阻止了对Monad接口的声明。

在努力理解单子的过程中,我阅读了Eric Lippert在Stack Overflow上对一个问题的回答,该问题问的是关于单子的简单解释。在那里,他还列出了可以在单子上执行的操作:

  1. 有一种方法可以将未放大类型的值转换为放大类型的值。
  2. 有一种方法可以将未放大类型上的操作转换为放大类型上的操作,这些操作遵守之前提到的函数组合的规则。
  3. 通常可以从放大类型中取回未放大类型。(这最后一点不是单子必需的,但通常存在这样的操作。)

阅读更多关于单子的资料后,我将第一个操作标识为return函数,将第二个操作标识为bind函数。我没有找到常用的第三个操作名称,所以我将其称为unbox函数。

为了更好地理解单子,我尝试在Java中声明一个泛型的Monad接口。为此,我首先查看了上述三个函数的签名。对于单子M,它看起来像这样:

return :: T1 -> M

bind :: M -> (T1 -> M) -> M

unbox :: M -> T1

return函数不是在M的实例上执行的,所以它不属于Monad接口。相反,它将被实现为构造函数或工厂方法。

现在,我暂时忽略接口声明中的unbox函数,因为它不是必需的。对于不同的接口实现,将有不同的该函数实现。

因此,Monad接口只包含bind函数。

让我们尝试声明这个接口:

public interface Monad {
    Monad bind();
}

有两个缺陷:

  • bind函数应该返回具体的实现,但它只返回接口类型。这是一个问题,因为我们在具体的子类型上声明了unbox操作。我将这称为问题1
  • bind函数应该接受一个函数作为参数。我们稍后来解决这个问题。

在接口声明中使用具体类型

这解决了问题1:如果我对单子的理解是正确的,那么bind函数总是返回一个与调用它的单子相同具体类型的新单子。所以,如果我有一个名为MMonad接口的实现,那么M.bind将返回另一个M而不是Monad。我可以使用泛型来实现这一点:

public interface Monad> {
    M bind();
}
public class MonadImpl> implements Monad {
    @Override
    public M bind() { /* do stuff and return an instance of M */ }
}

起初,这似乎行得通,然而至少有两个缺陷:

  • 这在实现类不提供自身而是提供Monad接口的另一个实现作为类型参数M时会出现问题,因为bind方法将返回错误的类型。例如:

    public class FaultyMonad> implements Monad { ... }
    

    将返回一个MonadImpl的实例,而不是一个FaultyMonad的实例。然而,我们可以在文档中指定这个限制,并将这样的实现视为程序员的错误。

  • 第二个缺陷更难解决。我将其称为问题2:当我尝试实例化MonadImpl类时,我需要提供M的类型。让我们来试试这个:

    new MonadImpl>>>>()
    

    为了获得有效的类型声明,这个过程必须无限进行下去。这里是另一个尝试:

    public static > MonadImpl create() {
        return new MonadImpl();
    }
    

    虽然这似乎行得通,但我们只是把问题推给了调用者。下面是我唯一能用这个函数做到的用法:

    public void createAndUseMonad() {
        MonadImpl monad = create();
        // use monad
    }
    

    本质上,这可以简化为

    MonadImpl monad = new MonadImpl<>();
    

    但这显然不是我们想要的。

在自身声明中使用具有转移类型参数的类型

现在,让我们为bind函数添加函数参数:如上所述,bind函数的签名如下:T1 -> M。在Java中,这是类型Function>。这是使用参数声明接口的第一次尝试:

public interface Monad> {
    M bind(Function function);
}

我们必须将类型T1作为泛型类型参数添加到接口声明中,以便在函数签名中使用它。第一个?是返回的类型为M的单子的T1。为了用T2替换它,我们必须添加T2本身作为泛型类型参数:

public interface Monad,
                       T2> {
    M bind(Function function);
}

现在,我们遇到了另一个问题。我们向Monad接口添加了第三个类型参数,因此我们必须在使用它时添加一个新的?。我们现在忽略新的?来研究第一个?。它是返回的类型为M的单子的M。让我们通过将M重命名为M1并引入另一个M2来尝试删除这个?

public interface Monad,
                       T2, M2 extends Monad< ?,  ?, ?, ?>> {
    M1 bind(Function function);
}

通过引入另一个T3,我们得到了:

public interface Monad,
                       T2, M2 extends Monad,
                       T3> {
    M1 bind(Function function);
}

再引入另一个M3,我们得到了:

public interface Monad,
                       T2, M2 extends Monad,
                       T3, M3 extends Monad< ?,  ?,  ?,  ?, ?, ?>> {
    M1 bind(Function function);
}

我们看到,如果我们尝试解决所有的?,这个过程将永远进行下去。这是问题3

总结

我们确定了三个问题:

  1. 在抽象类型的声明中使用具体类型。
  2. 实例化接收自身作为泛型类型参数的类型。
  3. 声明一个使用自身在其声明中具有转移类型参数的类型。

问题是:Java类型系统缺少什么特性?由于有一些支持单子的语言,这些语言必须以某种方式声明Monad类型。其他语言是如何声明Monad类型的?我没有找到关于这个的信息。我只找到了关于具体单子的声明的信息,比如Maybe单子。

我有遗漏吗?我能否用Java类型系统正确解决其中的一个问题?如果我无法用Java类型系统解决问题2,那么Java为什么不警告我关于不可实例化的类型声明?


正如前面所述,这个问题是关于理解单子的问题。如果我对单子的理解是错误的,你可以给我一些提示,但请不要试图给出解释。如果我对单子的理解是错误的,那么上述问题仍然存在。

这个问题也不是关于是否可以在Java中声明Monad接口。这个问题已经在上面的Eric Lippert的SO回答中得到了解答:不能。这个问题是关于具体阻止我这么做的限制是什么。Eric Lippert将其称为高级类型,但我无法理解它们。

0