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

Why can the Monad interface not be declared in Java?

在你开始阅读之前:这个问题不是关于理解 monad,而是关于识别 Java 类型系统的局限性,它阻止了 Monad 接口的声明。


为了理解 monad,我阅读了 this Eric Lippert 对 monad 的简单解释的问题的回答。在那里,他还列出了可以在 monad 上执行的操作:

  1. That there is a way to take a value of an unamplified type and turn it into a value of the amplified type.
  2. That there is a way to transform operations on the unamplified type into operations on the amplified type that obeys the rules of functional composition mentioned before
  3. That there is usually a way to get the unamplified type back out of the amplified type. (This last point isn't strictly necessary for a monad but it is frequently the case that such an operation exists.)

阅读更多有关 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();
}

有两个缺陷:

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

这解决了问题 1:如果我对 monad 的理解是正确的,那么 bind 函数总是 return 是一个与调用它的 monad 具有相同具体类型的新 monad。所以,如果我有一个名为 MMonad 接口的实现,那么 M.bind 将 return 另一个 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 */ }
}

起初,这似乎可行,但至少有两个缺陷:

在其自己的声明中使用一个带有移位类型参数的类型

现在,让我们将函数参数添加到 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 的 returned monad 的 T1。要用 T2 替换它,我们必须添加 T2 本身作为泛型类型参数:

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

现在,我们遇到了另一个问题。我们在 Monad 接口中添加了第三个类型参数,因此我们必须在其用法中添加一个新的 ? 。我们暂时忽略新的 ?,先研究现在的 ?。它是 M 类型的 returned 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

总结一下

我们确定了三个问题:

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

问题是:Java 类型系统缺少什么功能?由于存在与 monad 一起使用的语言,因此这些语言必须以某种方式声明 Monad 类型。这些其他语言如何声明 Monad 类型?我无法找到有关此的信息。我只找到有关具体 monad 声明的信息,例如 Maybe monad。

我错过了什么吗?我可以使用 Java 类型系统正确解决这些问题之一吗?如果我不能用 Java 类型系统解决问题 2,为什么 Java 不警告我不可实例化的类型声明?


如前所述,这个问题不是关于理解单子的问题。如果我对 monad 的理解是错误的,你可能会给出提示,但不要试图给出解释。如果我对 monad 的理解是错误的,那么所描述的问题仍然存在。

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

Most OOP languages do not have a rich enough type system to represent the monad pattern itself directly; you need a type system that supports types that are higher types than generic types. So I wouldn't try to do that. Rather, I would implement generic types that represent each monad, and implement methods that represent the three operations you need: turning a value into an amplified value, turning an amplified value into a value, and transforming a function on unamplified values into a function on amplified values.

如果您查看 AspectJ 项目正在做的事情,它类似于将 monads 应用于 Java。他们这样做的方式是 post- 处理 classes 的字节码以添​​加额外的功能——他们必须这样做的原因是因为没有办法 在没有 AspectJ 扩展的语言中 做他们需要做的事情;语言表达不够

一个具体的例子:假设你从 class A 开始。你有一个 monad M,使得 M(A) 是一个 class ,它像 A 一样工作,但是所有方法入口和出口追踪到 log4j。 AspectJ 可以做到这一点,但是 Java 语言本身没有任何工具可以让你这样做。

This paper describes how Aspect-Oriented Programming as in AspectJ might be formalized as monads

特别是,在 Java 语言中无法以编程方式指定类型(缺少字节码操作 a la AspectJ)。所有类型都是在程序启动时预先定义的。

What is the feature that is missing in the Java type system? How do these other languages declare the Monad type?

好问题!

Eric Lippert refers to this as higher types, but I can't get my head around them.

你并不孤单。但他们其实并没有听起来那么疯狂

让我们通过查看 Haskell 如何声明 monad "type" 来回答您的两个问题——稍后您就会明白为什么使用引号。我稍微简化了它;标准的 monad 模式在 Haskell:

中还有一些其他操作
class Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
  return :: a -> m a

男孩,这看起来既简单又完全不透明,不是吗?

在这里,让我再简化一下。 Haskell 允许您为 bind 声明自己的中缀运算符,但我们将其称为 bind:

class Monad m where
  bind :: m a -> (a -> m b) -> m b
  return :: a -> m a

好吧,现在至少我们可以看到里面有两个monad操作。剩下的是什么意思?

正如您所注意到的,第一个让您头脑清醒的是 "higher kinded types"。 (正如 Brian 指出的那样,我在我原来的回答中稍微简化了这个行话。同样很有趣的是你的问题引起了 Brian 的注意!)

在Java中,"class"是"type"的种类,而class可能是通用的。所以在 Java 中我们有 intIFrobList<IBar> 它们都是类型。

从现在开始,抛开你对 Giraffe 是 class 是 Animal 的子class 的任何直觉,等等;我们不需要那个。想想一个没有继承权的世界;它不会再次出现在这个讨论中。

Java 中的 class 是什么?好吧,想到 class 的最简单方法是它是一个 名称 ,用于 具有共同点的一组值 ,以便在需要 class 的实例时可以使用这些值中的任何一个。你有一个 class Point,比方说,如果你有一个 Point 类型的变量,你可以将 Point 的任何实例分配给它。 Point class 在某种意义上只是描述 所有 Point 个实例的集合 的一种方式。 类 是 高于实例 .

的东西

在Haskell中还有泛型和非泛型。 Haskell中的class是不是的一种类型。在Java中,一个class描述了一组;任何时候您需要 class 的实例时,您都可以使用该类型的值。在 Haskell 中,一个 class 描述了一组 类型 。这是 Java 类型系统缺失的关键特性。在Haskell 中,class 高于类型,即高于实例。 Java 只有两层层次结构; Haskell 有三个。在Haskell中你可以表达想法"any time I need a type that has certain operations, I can use a member of this class".

(旁白:我想在这里指出,我有点过于简单化了。考虑 Java 例如 List<int>List<String>。这是两个 "types",但是 Java 认为它们是一个 "class",所以在某种意义上 Java 也有 class 类型,它们比 "higher" 类型。但是然后同样,你可以在 Haskell 中说同样的话,即 list xlist y 是类型,而 list 是比类型更高的东西;它是可以产生一个类型。所以实际上更准确地说 Java 有 三层 层,而 Haskell 有 。重点仍然是:Haskell 有一个概念,它描述了比 Java 更强大的类型上可用的操作。我们将在下面更详细地讨论这个问题。)

那么这与界面有何不同?这听起来像是 Java 中的接口——您需要一个具有特定操作的类型,然后定义一个描述这些操作的接口。我们将看看 Java 接口缺少什么。

现在我们可以开始理解这个了 Haskell:

class Monad m where

那么,什么是Monad?这是一个class。什么是 class?它是一组具有共同点的类型,因此无论何时您需要具有特定操作的类型,都可以使用 Monad 类型。

假设我们有一个类型是这个 class 的成员;称之为 m。为了使该类型成为 class Monad 的成员,必须对此类型执行哪些操作?

  bind :: m a -> (a -> m b) -> m b
  return :: a -> m a

操作名称在 :: 左侧,签名在右侧。所以要成为一个 Monad,一个类型 m 必须有两个操作:bindreturn。这些操作的特征是什么?先来看return

  a -> m a

m a 是 Haskell,因为 Java 中的内容是 M<A>。也就是说,这意味着 m 是泛型类型,a 是类型,m am 参数化 a

Haskell 中的

x -> y 是 "a function which takes type x and returns type y" 的语法。是 Function<X, Y>.

把它们放在一起,我们有 return 是一个接受类型 a 参数和 returns 类型 m a 值的函数。或者在 Java

static <A>  M<A> Return(A a);

bind有点难。我认为 OP 很好地理解了这个签名,但是对于不熟悉简洁 Haskell 语法的读者,让我稍微扩展一下。

在Haskell中,函数只有一个参数。如果你想要一个有两个参数的函数,你可以创建一个接受一个参数的函数和 returns 另一个一个参数的函数。所以如果你有

a -> b -> c

那你得到了什么?接受 a 和 returns 和 b -> c 的函数。所以假设你想创建一个函数,它接受两个数字并返回它们的和。您将创建一个接受第一个数字的函数,并 returns 一个接受第二个数字并将其添加到第一个数字的函数。

在 Java 你会说

static <A, B, C>  Function<B, C> F(A a)

所以如果你想要一个 C 而你有一个 A 和一个 B,你可以说

F(a)(b)

有道理吗?

好吧,所以

  bind :: m a -> (a -> m b) -> m b

实际上是一个接受两个东西的函数:m aa -> m b 以及 returns 和 m b。或者,在Java中,直接是:

static <A, B> Function<Function<A, M<B>>, M<B>> Bind(M<A>)

或者,在 Java 中更惯用:

static <A, B> M<B> Bind(M<A>, Function<A, M<B>>) 

所以现在你明白为什么 Java 不能直接表示 monad 类型了。它没有能力说 "I have a class of types that have this pattern in common".

现在,您可以在 Java 中创建您想要的所有单子类型。你不能做的是制作一个代表想法的界面"this type is a monad type"。您需要做的是:

typeinterface Monad<M>
{
  static <A>    M<A> Return(A a);
  static <A, B> M<B> Bind(M<A> m, Function<A, M<B>> f);
}

看到类型接口如何谈论泛型本身了吗? monadic 类型是任何类型 M,它具有一个类型参数 具有这两个 static 方法。但是您不能在 Java 或 C# 类型系统中这样做。 Bind 当然可以是将 M<A> 作为 this 的实例方法。但是没有办法让 Return 变成静态的。 Java 使您无法 (1) 通过 unconstructed 泛型参数化接口,以及 (2) 无法指定静态成员是接口契约的一部分。

Since there are languages which work with monads, these languages have to somehow declare the Monad type.

嗯,你会这么想,但实际上不是。首先,当然,任何具有足够类型系统的语言都可以定义单子类型;你可以在 C# 或 Java 中定义你想要的所有 monadic 类型,你只是不能说出它们在类型系统中的共同点。例如,您不能制作只能由单子类型参数化的通用 class。

其次,您可以通过其他方式将 monad 模式嵌入到语言中。 C# 没有办法说 "this type matches the monad pattern",但 C# 语言中内置了查询理解 (LINQ)。查询理解适用于任何单子类型!只是bind操作要调用SelectMany,有点奇怪。但是如果你看一下 SelectMany 的签名,你会发现它只是 bind:

  static IEnumerable<R> SelectMany<S, R>(
    IEnumerable<S> source,
    Func<S, IEnumerable<R>> selector)

这是序列 monad SelectMany 的实现,IEnumerable<T>,但在 C# 中,如果您编写

from x in a from y in b select z

那么a的类型可以是any monadic类型,而不仅仅是IEnumerable<T>。需要的是 aM<A>bM<B>,并且有一个合适的 SelectMany 遵循 monad 模式。所以这是在语言中嵌入 "monad recognizer" 的另一种方式,而不是直接在类型系统中表示它。

(上一段实际上是一个过于简单化的谎言;出于性能原因,此查询使用的绑定模式与标准 monadic 绑定略有不同。概念上 这识别了 monad模式;实际上细节略有不同。如果您有兴趣,请在此处 http://ericlippert.com/2013/04/02/monads-part-twelve/ 阅读它们。)

还有几个小点:

I was not able to find a commonly used name for the third operation, so I will just call it the unbox function.

不错的选择;它通常被称为 "extract" 操作。 monad 不需要公开提取操作,但当然 bind 需要能够从 M<A> 中获取 A为了在其上调用 Function<A, M<B>>,所以逻辑上通常存在某种提取操作。

A comonad——从某种意义上说,一个向后的 monad——需要公开一个 extract 操作; extract 本质上是 return 向后。 comonad 也需要一个 extend 操作,有点像 bind 倒退。它具有签名 static M<B> Extend(M<A> m, Func<M<A>, B> f)

真是个好问题! :-)

正如@EricLippert 指出的那样,Haskell 中称为 "type classes" 的多态类型超出了 Java 的类型系统的范围。然而,至少自从引入 Frege 编程语言以来,已经表明确实可以在 JVM 之上实现类似 Haskell 的类型系统。

如果你想在 Java 语言本身中使用更高级的类型,你必须求助于像 highJ or Cyclops. Both libraries do provide a monad type class in the Haskell sense (see here and here, respectively, for the sources of the monad type class). In both cases, be prepared for some major syntactic inconveniences; this code will not look pretty at all and carries a lot of overhead to shoehorn this functionality into Java's type system. Both libraries use a "type witness" to capture the core type separately from the data type, as John McClean explains in his excellent introduction 这样的库。但是,在这两种实现中,您都找不到像 Maybe extends MonadList extends Monad.

这样简单明了的东西

使用 Java 接口指定构造函数或静态方法的次要问题可以通过引入将静态方法声明为非静态方法的工厂(或 "companion")接口轻松解决。就个人而言,我总是尽量避免任何静态的东西,而是使用注入的单例。

长话短说,是的,可以在 Java 中表示 HKT,但在这一点上非常不方便且对用户不太友好。