【问题标题】:Why can the Monad interface not be declared in Java?为什么 Java 中不能声明 Monad 接口?
【发布时间】:2016-03-11 23:36:48
【问题描述】:

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


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

  1. 有一种方法可以获取未放大类型的值并将其转换为放大类型的值。
  2. 有一种方法可以将未放大类型的操作转换为放大类型的操作,该操作遵循前面提到的函数组合规则
  3. 通常有一种方法可以将未放大的类型从放大的类型中恢复出来。 (最后一点对于 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。所以,如果我有一个名为MMonad 接口的实现,那么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 -&gt; M&lt;T2&gt;。在 Java 中,这是 Function&lt;T1, M&lt;T2&gt;&gt; 类型。下面是第一次尝试用参数声明接口:

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

总结一下

我们发现了三个问题:

  1. 在抽象类型的声明中使用具体类型。
  2. 实例化接收自身作为泛型类型参数的类型。
  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


【解决方案1】:

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中我们有intIFrobList&lt;IBar&gt;,它们都是类型。

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

Java 中的类是什么?嗯,考虑一个类的最简单的方法是,它是一个 name,代表一组具有共同点的 ,因此这些值中​​的任何一个都可以是当需要类的实例时使用。假设你有一个类Point,如果你有一个Point 类型的变量,你可以将Point 的任何实例分配给它。 Point 类在某种意义上只是描述所有Point 实例的集合的一种方式。类是高于实例的东西

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

(旁白:我想在这里指出我有点过于简单化了。例如在 Java 中考虑 List&lt;int&gt;List&lt;String&gt;。这是两种“类型”,但 Java 认为它们是一种“类”,所以在某种意义上,Java 也有比类型“更高”的类。但话又说回来,你可以在 Haskell 中说同样的话,list xlist y 是类型,而 list 是一个东西比类型更高;它是可以产生类型的东西。所以实际上更准确地说Java有三个级别,而Haskell有四个 . 重点仍然存在: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 是 Haskell,在 Java 中是 M&lt;A&gt;。也就是说,这意味着m 是一个泛型类型,a 是一个类型,m am 参数化了a

x -&gt; y 在 Haskell 中的语法是“一个接受类型 x 并返回类型 y 的函数”。我是Function&lt;X, Y&gt;

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

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

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

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

a -> b -> c

那你有什么?一个接受a 并返回b -&gt; 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

实际上是一个接受两件事的函数:m aa -&gt; m b,它返回一个 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&lt;A&gt; 作为this 的实例方法。但是除了静态之外,没有办法使Return 成为任何东西。 Java 无法让您 (1) 通过 非构造 泛型类型参数化接口,以及 (2) 无法指定静态成员是接口契约的一部分。

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

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

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

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

那是序列单子IEnumerable&lt;T&gt;SelectMany 的实现,但是如果你写的话是在C# 中

from x in a from y in b select z

那么a 的类型可以是任何 monadic 类型,而不仅仅是IEnumerable&lt;T&gt;。需要的是aM&lt;A&gt;bM&lt;B&gt;,并且有一个合适的SelectMany 遵循monad 模式。所以这是在语言中嵌入“monad 识别器”的另一种方式,而不是直接在类型系统中表示它。

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

还有几个小点:

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

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

comonad——从某种意义上说是一个向后的单子——需要暴露extract 操作; extract 本质上是 return 倒退。一个comonad 也需要一个extend 操作,它有点像bind 倒转。它有签名static M&lt;B&gt; Extend(M&lt;A&gt; m, Func&lt;M&lt;A&gt;, B&gt; f)

【讨论】:

  • 感谢您的精彩回答!尤其是关于 Haskells 三级类型系统的解释以及您对typeinterface 的编造符号对我的理解有很大帮助。
  • @EricLippert,可以使用“MonadFactory”基类来解决 C# 或 Java 中的这个限制吗?除了简洁的语法,相对于真正的高级类型系统,MonadFactory 不能做什么?
  • @EduardoS:我不知道;试一试并报告您的发现!
  • typeinterface 表示法中:ReturnBind 函数的类型参数 A 是否相同?该符号表示它们可以不同。
  • 嗯,它们是通用的。如果您手头有int,那么您可以获得M&lt;int&gt;。如果您手头有M&lt;int&gt;Func&lt;int, M&lt;String&gt;&gt;,那么您可以获得M&lt;string&gt;。不过,我不太确定我是否在这里遵循您的思路。
【解决方案2】:

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

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

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

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

【讨论】:

  • 虽然这不能回答问题,但我很欣赏这个提示。我想阅读这篇论文,但是链接已损坏。
【解决方案3】:

确实是好问题! :-)

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

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

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

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

【讨论】:

    猜你喜欢
    • 2010-09-06
    • 2011-11-02
    • 2017-09-10
    • 2013-01-27
    • 1970-01-01
    • 1970-01-01
    • 2011-01-23
    • 1970-01-01
    相关资源
    最近更新 更多