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

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


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

  1. 有一种方法可以获取非放大类型的值并将其转换为放大类型的值。
  2. 有一种方法可以将非放大类型的操作转换为符合之前提到的function组合规则的放大类型的操作
  3. 通常有一种方法可以将未放大的类型从放大类型中取出。 (对于monad来说,最后一点并不是绝对必要的,但通常存在这样的操作。)

在阅读了关于monads的更多信息后,我将第一个操作识别为return函数,将第二个操作识别为bind函数。 我无法找到第三个操作的常用名称,因此我将其称为unbox函数。

为了更好地理解monad,我继续尝试用Java声明一个通用的Monad接口。 为此,我首先看了上面三个函数的签名。 对于Monad M ,它看起来像这样:

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

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

此外,我现在省略了接口声明中的unbox函数,因为它不是必需的。 对于接口的不同实现,将具有该function的不同实现。

因此, Monad接口仅包含bindfunction。

让我们尝试声明接口:

 public interface Monad { Monad bind(); } 

有两个缺点:

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

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

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

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

起初,这似乎有效,但至少有两个缺点:

  • 一旦实现类没有自己提供,而Monad接口的另一个实现作为类型参数M ,这就会中断,因为然后bind方法将返回错误的类型。 比如说

     public class FaultyMonad<M extends MonadImpl> implements Monad { ... } 

    将返回一个MonadImpl实例,它应该返回一个FaultyMonad实例。 但是,我们可以在文档中指定此限制,并将此类实现视为程序员错误。

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

     new MonadImpl<MonadImpl<MonadImpl<MonadImpl<MonadImpl>>>>() 

    要获得有效的类型声明,必须无限继续。 这是另一种尝试:

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

    虽然这似乎有效,但我们只是将问题推迟到被叫方。 以下是该function对我有用的唯一用法:

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

    这基本上归结为

     MonadImpl monad = new MonadImpl(); 

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

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

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

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

我们必须将类型T1作为generics类型参数添加到接口声明中,因此我们可以在函数签名中使用它。 第一个?M类型的返回monad的T1 。 要用T2替换它,我们必须将T2本身添加为generics类型参数:

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

现在,我们遇到了另一个问题。 我们在Monad接口中添加了第三个类型参数,所以我们必须添加一个新参数? 使用它。 我们会忽略新的? 现在先调查一下? 。 它是M型返回monad的M 我们试着删除这个? 通过将M重命名为M1并引入另一个M2

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

介绍另一个T3导致:

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

并引入另一个M3结果:

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

我们看到,如果我们试图解决所有问题,这将会永远持续下去? 。 这是问题3

总结一下

我们确定了三个问题:

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

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

我错过了什么吗? 我可以使用Java类型系统正确解决其中一个问题吗? 如果我不能用Java类型系统解决问题2,那么为什么Java没有警告我关于不可实例化的类型声明?


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

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

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

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

好问题!

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

你不是一个人。 但它们实际上并不像听起来那么疯狂。

让我们通过查看Haskell如何声明monad“类型”来回答你的两个问题 – 你会在一分钟内看到为什么引用。 我有点简化了; 标准monad模式在Haskell中还有一些其他操作:

 class Monad m where (>>=) :: ma -> (a -> mb) -> mb return :: a -> ma 

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

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

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

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

正如你所注意到的,首先要理解的是“更高级的类型”。 (正如Brian指出的那样,我在原来的答案中简化了这个术语。同样很有趣的是你的问题引起了Brian的注意!)

在Java中,“类”是一种 “类型”,类可以是通用的。 所以在Java中我们有intIFrob以及List ,它们都是类型。

从这一点开始,你可以放弃任何关于长颈鹿作为动物子类的等级的直觉,等等; 我们不需要那样做。 想想一个没有inheritance的世界; 它不会再次进入这个讨论。

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

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

(旁白:我想在这里指出我有点过于简单化。在Java中考虑例如ListList 。这是两个“类型”,但Java认为它们是一个“类” “,所以在某种意义上,Java也有比类型”更高“的类。但是再次,你可以在Haskell中说同样的, list xlist y是类型,并且该list高于a类型;它是一个可以生成类型的东西。所以事实上说,Java有三个级别,而Haskell有四个级别更准确。但问题仍然存在:Haskell有一个描述类型的可用操作的概念比Java更强大。我们将在下面详细介绍。)

那么这与界面有何不同? 这听起来像Java中的接口 – 您需要具有某些操作的类型,您可以定义描述这些操作的接口。 我们将看到Java接口缺少的东西。

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

 class Monad m where 

那么, Monad是什么? 这是一堂课。 什么是课程? 它是一组具有共同点的类型,这样无论何时需要具有某些操作的类型,都可以使用Monad类型。

假设我们有一个类是该类的成员; 叫它。 为了使该类型成为Monad类的成员,必须在此类型上执行哪些操作?

  bind :: ma -> (a -> mb) -> mb return :: a -> ma 

操作的名称位于::的左侧,签名位于右侧。 所以要成为Monad ,类型m必须有两个操作: bindreturn 。 这些行动的签名是什么? 让我们先看看return

  a -> ma 

ma是Haskell,因为Java中的内容是M 。 也就是说,这意味着m是generics类型, a是类型, ma是用a参数化a

Haskell中的x -> y是“一个接受类型x并返回类型y的函数”的语法。 它的Function

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

 static  M Return(A a); 

bind有点困难。 我认为OP很好地理解了这个签名,但对于那些不熟悉简洁的Haskell语法的读者,让我对此进行一些扩展。

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

 a -> b -> c 

那你有什么? 取a并返回b -> c函数。 所以假设你想制作一个带两个数字的函数并返回它们的总和。 您将创建一个取第一个数字的函数,并返回一个取第二个数字并将其添加到第一个数字的函数。

在Java中你会说

 static  Function F(A a) 

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

 F(a)(b) 

合理?

好的,所以

  bind :: ma -> (a -> mb) -> mb 

实际上是一个带有两个东西的函数:一个ma ,一个a -> mb ,它返回一个mb 。 或者,在Java中,它直接:

 static  Function>, M> Bind(M) 

或者,在Java中更具惯用性:

 static  M Bind(M, Function>) 

所以现在你明白为什么Java不能直接表示monad类型。 它没有能力说“我有一类具有这种共同模式的类型”。

现在,您可以在Java中创建所需的所有monadic类型。 你不能做的事情是创建一个代表“这种类型是monad类型”的想法的界面。 你需要做的是:

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

看看类型接口如何讨论generics类型本身? monadic类型是任何类型M ,它具有一个类型参数的通用, 具有这两种静态方法。 但是你不能在Java或C#类型系统中这样做。 Bind当然可以是一个以M为例的实例方法。 但是除了静态之外,没有办法让Return变成任何东西。 Java使您无法(1)通过未构造的generics类型参数化接口,以及(2)无法指定静态成员是接口契约的一部分。

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

那么你会这么认为,但事实并非如此。 首先,当然任何具有足够类型系统的语言都可以定义monadic类型; 你可以在C#或Java中定义你想要的所有monadic类型,你只是不能说它们在类型系统中有什么共同之处。 例如,您不能创建只能由monadic类型参数化的generics类。

其次,您可以通过其他方式将monad模式嵌入到语言中。 C#没有办法说“这种类型匹配monad模式”,但C#内置了该语言的查询理解(LINQ)。 查询理解适用于任何monadic类型! 只是绑定操作必须被称为SelectMany ,这SelectMany 。 但是如果你看一下SelectMany的签名,你会发现它只是bind

  static IEnumerable SelectMany( IEnumerable source, Func> selector) 

这是序列monad, IEnumerableSelectMany实现,但是在C#中如果你写的话

 from x in a from y in b select z 

那么a类型可以是任何 monadic类型,而不仅仅是IEnumerable 。 需要的是aMbM ,并且存在遵循monad模式的合适的SelectMany 。 这就是在语言中嵌入“monad Recognizer”的另一种方式,而不是直接在类型系统中表示它。

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

还有几个小点:

我无法找到第三个操作的常用名称,因此我将其称为unbox函数。

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

comonad – 从某种意义上说是一个向后的monad – 需要extract一个extract操作; extract基本上是向后退。 comonad也需要一个extend操作,它是一种向后转向的bind 。 它具有签名static M Extend(M m, Func, B> f)

如果你看看AspectJ项目正在做什么,它类似于将monads应用于Java。 他们这样做的方法是对类的字节代码进行后处理以添加其他function – 他们必须这样做的原因是因为没有AspectJ扩展在语言中做他们需要做的事情是没有办法做; 语言不够富有表现力。

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

本文描述了AspectJ中面向方面编程如何forms化为monad

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