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

Ste*_*ase 36 java generics monads type-systems higher-order-types

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


在我的努力去理解单子我读这个由埃里克利珀SO-答案上询问单子简单的解释问题.在那里,他还列出了可以在monad上执行的操作:

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

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

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

return :: T1 -> M<T1>
bind   :: M<T1> -> (T1 -> M<T2>) -> M<T2>
unbox  :: M<T1> -> T1
Run Code Online (Sandbox Code Playgroud)

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

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

因此,Monad接口仅包含该bind功能.

让我们尝试声明接口:

public interface Monad {
    Monad bind();
}
Run Code Online (Sandbox Code Playgroud)

有两个缺点:

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

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

这解决了问题1:如果我对monads的理解是正确的,那么bind函数总是会返回一个与调用它的monad具有相同具体类型的新monad.所以,如果我有一个Monad被调用的接口的实现M,那么M.bind将返回另一个M但不是a 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 */ }
}
Run Code Online (Sandbox Code Playgroud)

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

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

    public class FaultyMonad<M extends MonadImpl<M>> implements Monad<M> { ... }
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    new MonadImpl<MonadImpl<MonadImpl<MonadImpl<MonadImpl< ... >>>>>()
    
    Run Code Online (Sandbox Code Playgroud)

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

    public static <M extends MonadImpl<M>> MonadImpl<M> create() {
        return new MonadImpl<M>();
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

    public void createAndUseMonad() {
        MonadImpl<?> monad = create();
        // use monad
    }
    
    Run Code Online (Sandbox Code Playgroud)

    这基本上归结为

    MonadImpl<?> monad = new MonadImpl<>();
    
    Run Code Online (Sandbox Code Playgroud)

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

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

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

public interface Monad<T1, M extends Monad<?, ?>> {
    M bind(Function<T1, M> function);
}
Run Code Online (Sandbox Code Playgroud)

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

public interface Monad<T1, M extends Monad<T2, ?, ?>,
                       T2> {
    M bind(Function<T1, M> function);
}
Run Code Online (Sandbox Code Playgroud)

现在,我们遇到了另一个问题.我们在Monad界面中添加了第三个类型参数,因此我们必须添加一个新?的用法.我们现在将忽略新?的现在首先调查?.它是M返回的monad类型M.让我们尝试删除此?重命名MM1,并通过引入另一M2:

public interface Monad<T1, M1 extends Monad<T2, M2, ?, ?>,
                       T2, M2 extends Monad< ?,  ?, ?, ?>> {
    M1 bind(Function<T1, M1> function);
}
Run Code Online (Sandbox Code Playgroud)

介绍另一个T3结果:

public interface Monad<T1, M1 extends Monad<T2, M2, T3, ?, ?>,
                       T2, M2 extends Monad<T3,  ?,  ?, ?, ?>,
                       T3> {
    M1 bind(Function<T1, M1> function);
}
Run Code Online (Sandbox Code Playgroud)

并引入另一个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);
}
Run Code Online (Sandbox Code Playgroud)

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

总结一下

我们确定了三个问题:

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

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

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


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

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

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

Eri*_*ert 39

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

好问题!

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

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

让我们通过查看Haskell如何声明monad"类型"来回答你的两个问题 - 你会在一分钟内看到引号的原因.我有点简化了; 标准的monad模式在Haskell中还有其他一些操作:

class Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
  return :: a -> m a
Run Code Online (Sandbox Code Playgroud)

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

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

class Monad m where
  bind :: m a -> (a -> m b) -> m b
  return :: a -> m a
Run Code Online (Sandbox Code Playgroud)

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

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

在Java中,"类"是一种 "类型",类可以是通用的.因此,在Java中,我们已经有了int,并IFrobList<IBar>他们是所有类型.

从这一点开始,你可以放弃任何关于长颈鹿是一个动物类的类的直觉,等等; 我们不需要那个.想想一个没有继承的世界; 它不会再次进入这个讨论.

Java中的类是什么?好了,想一类最简单的方法是,它是一个名称组有共同之处值,使得这些值的任何一个都可以在需要的类的实例中使用.Point比方说,你有一个类,如果你有一个类型的变量Point,你可以Point为它分配任何实例.从Point某种意义上说,这个类只是描述所有Point实例集的一种方式.类是比实例更高的东西.

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

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

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

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

class Monad m where
Run Code Online (Sandbox Code Playgroud)

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

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

  bind :: m a -> (a -> m b) -> m b
  return :: a -> m a
Run Code Online (Sandbox Code Playgroud)

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

  a -> m a
Run Code Online (Sandbox Code Playgroud)

m aHaskell是Java的用途M<A>.也就是说,这意味着m是一种泛型类型,a是一种类型,m am参数化的a.

x -> y在Haskell中是"一个接受类型x和返回类型的函数"的语法y.是的Function<X, Y>.

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

static <A>  M<A> Return(A a);
Run Code Online (Sandbox Code Playgroud)

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

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

a -> b -> c
Run Code Online (Sandbox Code Playgroud)

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

在Java中你会说

static <A, B, C>  Function<B, C> F(A a)
Run Code Online (Sandbox Code Playgroud)

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

F(a)(b)
Run Code Online (Sandbox Code Playgroud)

合理?

好的,所以

  bind :: m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

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

static <A, B> Function<Function<A, M<B>>, M<B>> Bind(M<A>)
Run Code Online (Sandbox Code Playgroud)

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

static <A, B> M<B> Bind(M<A>, Function<A, M<B>>) 
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

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

  static IEnumerable<R> SelectMany<S, R>(
    IEnumerable<S> source,
    Func<S,?IEnumerable<R>> selector)
Run Code Online (Sandbox Code Playgroud)

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

from x in a from y in b select z
Run Code Online (Sandbox Code Playgroud)

那么a类型可以是任何 monadic类型,而不仅仅是IEnumerable<T>.所需要的是该aM<A>,即b就是M<B>,并且有合适的 SelectMany随后的单子图案.这就是在语言中嵌入"monad识别器"的另一种方式,而不是直接在类型系统中表示它.

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

还有几个小点:

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

好的选择; 它通常被称为"提取"操作.一个单子不必具有提取操作暴露,但当然某种程度上bind需要能够得到A出来的M<A>,以调用Function<A, M<B>>就可以了,所以逻辑上某种提取操作通常存在.

comonad -向后单子,在某种意义上-需要一个extract被曝光操作; extract基本上是return倒退.一个comonad以及需要的extend是那种操作bind背过身去.它有签名static M<B> Extend(M<A> m, Func<M<A>, B> f)

  • 感谢您的好评!尤其是有关Haskells三层类型系统的解释以及您为`typeinterface`组成的符号对我的理解很有帮助。 (2认同)