37

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


在我努力理解 monad 的过程中,我阅读了 Eric Lippert 的这个SO-answer 关于一个关于 monad 的简单解释的问题。在那里,他还列出了可以在 monad 上执行的操作:

  1. 有一种方法可以获取未放大类型的值并将其转换为放大类型的值。
  2. 有一种方法可以将未放大类型的操作转换为放大类型的操作,该操作遵循前面提到的函数组合规则
  3. 通常有一种方法可以将未放大的类型从放大的类型中恢复出来。(最后一点对于 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来删除它:M1M2

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类型?我无法找到有关此的信息。我只找到有关具体单子声明的信息,例如Maybe单子。

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


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

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

大多数 OOP 语言没有足够丰富的类型系统来直接表示 monad 模式本身;您需要一个支持比泛型类型更高的类型的类型系统。所以我不会尝试那样做。相反,我将实现表示每个 monad 的泛型类型,并实现表示您需要的三个操作的方法:将值转换为放大值,将放大值转换为值,以及将未放大值上的函数转换为放大的值。

4

3 回答 3

43

Java 类型系统中缺少什么特性?这些其他语言如何声明 Monad 类型?

好问题!

Eric Lippert 将此称为高级类型,但我无法理解它们。

你并不孤单。但他们实际上并不像听起来那么疯狂。

让我们通过查看 Haskell 如何声明 monad“类型”来回答您的两个问题——您将在一分钟内了解为什么引用。我对它进行了一些简化;标准的 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 操作。剩下的这意味着什么?

正如您所指出的,首先要了解的是“高级类型”。(正如布赖恩指出的那样,我在最初的回答中稍微简化了这个行话。你的问题引起了布赖恩的注意也很有趣!)

在Java中,“类”是一种“类型”,一个类可能是泛型的。所以在Java 中我们有intandIFrobList<IBar>and 它们都是类型。

从这一点开始,抛开你对 Giraffe 是 Animal 的一个子类的任何直觉,等等;我们不需要那个。想想一个没有遗产的世界;它不会再次进入这个讨论。

Java 中的类是什么?好吧,考虑类的最简单方法是,它是一组具有共同点的值的名称,这样当需要类的实例时,可以使用这些值中的任何一个。假设你有一个 class ,如果你有一个 type 的变量,你可以为它分配任何实例。类在某种意义上只是描述所有实例集合的一种方式。类是高于实例的东西。PointPointPointPointPoint

在 Haskell 中,还有泛型和非泛型类型。Haskell 中的类不是一种类型。在 Java 中,一个类描述了一组;任何时候你需要一个类的实例,你都可以使用那个类型的值。在 Haskell 中,一个类描述了一组类型。这是 Java 类型系统缺少的关键特性。在 Haskell中,类高于类型,类型高于实例。Java只有两级层次结构;Haskell 有三个。在 Haskell 中,您可以表达“任何时候我需要具有某些操作的类型,我都可以使用此类的成员”的想法。

(旁白:我想在这里指出我有点过于简单化了。例如在 Java 中考虑List<int>List<String>。这是两种“类型”,但 Java 认为它们是一个“类”,所以在某种意义上 Java 也是具有比类型“更高”的类。但话又说回来,你可以在 Haskell 中说同样的话,list xandlist y是类型,这list是比类型更高的东西;它是可以产生类型的东西。所以它实际上更准确地说 Java 有3 个级别,而 Haskell 有4 个. 但重点仍然存在:Haskell 有一个描述可用操作的概念,该类型比 Java 更强大。我们将看看这在下面更详细。)

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

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

class Monad m where

那么,什么是Monad?这是一堂课。什么是班级?它是一组具有共同点的类型,因此每当您需要具有某些操作的类型时,都可以使用Monad类型。

假设我们有一个类型是这个类的成员;叫它m。为了使该类型成为该类的成员,必须对该类型进行哪些操作Monad

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

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

  a -> m a

m a在 Java 中是 Haskell M<A>。也就是说,这意味着m是一个泛型类型,a是一个类型,m am用 参数化的a

x -> yx在 Haskell 中是“接受类型并返回类型的函数”的语法y。是Function<X, Y>

把它放在一起,我们就有return了一个函数,它接受一个类型的参数a并返回一个类型的值m a。或者在 Java 中

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

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

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

a -> b -> c

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

在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

实际上是一个接受两件事的函数: anm a和 aa -> m b并且它返回 an 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 类型了。它没有能力说“我有一类具有这种共同模式的类型”。

现在,您可以在 Java 中创建所有想要的单子类型。你不能做的事情是制作一个代表“这种类型是单子类型”的接口。您需要做的是:

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

看看类型接口如何谈论泛型类型本身?一元类型是任何M具有一个类型参数的泛型类型具有这两个静态方法。但是你不能在 Java 或 C# 类型系统中这样做。Bind当然可以是一个采用M<A>as的实例方法thisReturn但是除了静态之外没有其他方法可以制作任何东西。Java 无法让您 (1) 通过非构造的泛型类型参数化接口以及 (2) 无法指定静态成员是接口契约的一部分。

由于有些语言可以使用 monad,因此这些语言必须以某种方式声明 Monad 类型。

好吧,您会这么认为,但实际上并非如此。首先,当然,任何具有足够类型系统的语言都可以定义一元类型;你可以在 C# 或 Java 中定义所有你想要的单子类型,你只是不能说出它们在类型系统中的共同点。例如,您不能创建只能由单子类型参数化的泛型类。

其次,您可以通过其他方式将 monad 模式嵌入到语言中。C# 无法说“这种类型与 monad 模式匹配”,但 C# 具有内置于语言中的查询理解 (LINQ)。查询推导适用于任何一元类型!只是必须调用绑定操作SelectMany,这有点奇怪。但是,如果您查看 的签名SelectMany,您会发现它只是bind

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

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

from x in a from y in b select z

thena的类型可以是任何一元类型,而不仅仅是IEnumerable<T>. 所需要的是aM<A>bM<B>并且有一个 SelectMany遵循单子模式的合适的。所以这是在语言中嵌入“monad 识别器”的另一种方式,而不是直接在类型系统中表示它。

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

还有几个小点:

我找不到第三个操作的常用名称,所以我将其称为 unbox 函数。

好的选择; 它通常被称为“提取”操作。monad不需要暴露提取操作,但当然bind需要能够以某种方式从 the中A取出M<A>以便调用Function<A, M<B>>它,因此从逻辑上讲,通常存在某种提取操作。

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

于 2016-03-12T15:57:57.177 回答
3

如果您查看AspectJ项目正在做什么,它类似于将 monad 应用于 Java。他们这样做的方式是对类的字节码进行后处理以添加额外的功能——而他们必须这样做的原因是因为在没有 AspectJ 扩展的情况下,语言内无法完成他们需要做的事情做; 语言不够表达。

一个具体的例子:假设你从类 A 开始。你有一个单子 M,这样 M(A) 是一个像 A 一样工作的类,但是所有方法的入口和出口都被跟踪到 log4j。AspectJ 可以做到这一点,但 Java 语言本身并没有允许您这样做的工具。

本文描述了 AspectJ 中的面向方面编程如何被形式化为 monad

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

于 2016-03-12T00:14:41.203 回答
2

确实是好问题!:-)

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

如果您想在 Java 语言本身中使用更高种类的类型,您必须求助于highJCyclops之类的库。这两个库都提供了 Haskell 意义上的 monad 类型类(请分别参见此处此处,了解 monad 类型类的来源)。在这两种情况下,都要为一些主要的语法不便做好准备;这段代码看起来一点也不漂亮,并且需要大量开销来将此功能硬塞到 Java 的类型系统中。正如 John McClean 在其出色的介绍中所解释的那样,这两个库都使用“类型见证”来将核心类型与数据类型分开捕获. Maybe extends Monad但是,在这两种实现中,您都不会发现像or一样简单明了的东西List extends Monad

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

长话短说,是的,可以用 Java 表示 HKT,但此时它非常不方便,也不是很用户友好。

于 2019-08-06T01:17:23.970 回答