java - 为什么不能用Java声明Monad接口(interface)?

标签 java generics monads type-systems higher-order-types

开始阅读之前:这个问题不是关于理解monad的问题,而是关于确定Java类型系统的局限性的,这会阻止声明Monad接口(interface)。

在理解单子(monad)的过程中,我读了Eric Lippert的this SO-answer,该问题询问单子(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接口(interface)。为此,我首先看了上面三个功能的签名。对于Monad M,它看起来像这样:
return :: T1 -> M<T1>
bind   :: M<T1> -> (T1 -> M<T2>) -> M<T2>
unbox  :: M<T1> -> T1
return函数不会在M的实例上执行,因此它不属于Monad接口(interface)。而是将其实现为构造函数或工厂方法。

同样现在,由于不需要,我从接口(interface)声明中省略了unbox函数。对于接口(interface)的不同实现,此功能将有不同的实现。

因此,Monad接口(interface)仅包含bind函数。

让我们尝试声明接口(interface):
public interface Monad {
    Monad bind();
}

有两个缺陷:
  • bind函数应返回具体的实现,但是仅返回接口(interface)类型。这是一个问题,因为在具体的子类型上声明了取消装箱操作。我将其称为问题1
  • bind函数应检索一个函数作为参数。我们稍后会解决。

  • 在接口(interface)声明中使用具体类型

    这解决了问题1:如果我对monad的理解是正确的,那么bind函数总是返回一个新的monad,其类型与调用它的monad相同。因此,如果我有一个称为MonadM接口(interface)的实现,则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接口(interface)的另一种实现作为类型参数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>>类型。这是用参数声明接口(interface)的第一次尝试:
    public interface Monad<T1, M extends Monad<?, ?>> {
        M bind(Function<T1, M> function);
    }
    

    我们必须将T1类型作为通用类型参数添加到接口(interface)声明中,以便可以在函数签名中使用它。第一个?是类型为T1的返回单子(monad)的M。要用T2替换它,我们必须添加T2本身作为通用类型参数:
    public interface Monad<T1, M extends Monad<T2, ?, ?>,
                           T2> {
        M bind(Function<T1, M> function);
    }
    

    现在,我们遇到了另一个问题。我们在Monad接口(interface)中添加了第三个类型参数,因此我们必须为其用法添加新的?。现在,我们将忽略新的?,以调查现在的第一个?。它是返回的M类型的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

    总结一下

    我们确定了三个问题:
  • 在抽象类型的声明中使用具体类型。
  • 实例化一个将其自身接收为通用类型参数的类型。
  • 声明一个类型,该类型在其带有移位类型参数的声明中使用自身。

  • 问题是:Java类型系统缺少哪些功能?由于存在适用于monad的语言,因此这些语言必须以某种方式声明Monad类型。这些其他语言如何声明Monad类型?我找不到有关此的信息。我只找到有关具体单子(monad)声明的信息,例如Maybe单子(monad)。

    我想念什么吗?我可以使用Java类型系统正确解决这些问题之一吗?如果我不能使用Java类型系统解决问题2,那么Java是否有理由不警告我关于不可实例化的类型声明?

    如前所述,关于理解单子(monad)的问题不是,而是。如果我对单子(monad)的理解是错误的,您可能会对此有所提示,但不要尝试给出解释。如果我对单子(monad)的理解是错误的,那么所描述的问题仍然存在。

    这个问题也不是关于是否可以在Java中声明Monad接口(interface)。这个问题已经由Eric Lippert在他上方的SO-答案中得到了答案:并非如此。这个问题是关于阻止我执行此操作的限制到底是什么。埃里克·利珀特(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.

    最佳答案

    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“类型”来回答您的两个问题-您将在一分钟内看到为什么引用。我做了一些简化。标准monad模式在Haskell中还有其他几个操作:
    class Monad m where
      (>>=) :: m a -> (a -> m b) -> m b
      return :: a -> m a
    

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

    在这里,让我简化一下。 Haskell让您声明自己的infix运算符进行绑定(bind),但是我们将其称为bind:
    class Monad m where
      bind :: m a -> (a -> m b) -> m b
      return :: a -> m a
    

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

    如您所知,首先要注意的是“更高种类的类型”。 (正如Brian指出的那样,我在原始答案中稍微简化了该行话。您的问题引起了Brian的注意也很有趣!)

    在Java中,“类”是“类型”的一种,并且类可以是通用的。因此,在Java中,我们得到了intIFrobList<IBar>,它们都是类型。

    从这时起,您就不再有关于长颈鹿是动物的子类的类的直觉了,依此类推;我们不需要。想想一个没有继承的世界。它不会再进入此讨论。

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

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

    (ASIDE:我想在这里指出我有点过分简化了。考虑在Java中,例如List<int>List<String>。这是两个“类型”,但是Java认为它们是一个“类”,因此在某种意义上Java还具有比类型“高”的类,但是再一次,您可以在Haskell中说同样的话,list xlist y是类型,而list是比类型高的东西;这是可以产生的东西因此,实际上说Java有3个层次,而Haskell有4个层次实际上是更准确的,尽管如此,重点仍然是:Haskell有一个概念来描述在类型上比Java更强大的类型上可用的操作。我们将在下面对此进行更详细的介绍。)

    那么这与接口(interface)有何不同?这听起来像Java中的接口(interface)-您需要一种具有某些操作的类型,然后定义一个描述这些操作的接口(interface)。我们将看到Java接口(interface)缺少的内容。

    现在我们可以开始理解这个Haskell了:
    class Monad m where
    

    那么,Monad是什么?这是一个类。什么是类(class)?这是一组具有一些共同点的类型,因此,每当需要具有某些操作的类型时,都可以使用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<A>。也就是说,这意味着m是泛型类型,a是类型,m a是用m参数化的a

    Haskell中的x -> y是“采用x类型并返回y类型的函数”的语法。这是Function<X, Y>

    放在一起,我们就有了return这个函数,它接受a类型的参数并返回m a类型的值。或用Java
    static <A>  M<A> Return(A a);
    
    bind有点难。我认为OP非常了解此签名,但是对于不熟悉简洁的Haskell语法的读者,让我对其进行扩展。

    在Haskell中,函数仅采用一个参数。如果要使用两个自变量的函数,则可以创建一个接受一个自变量并返回另一个具有一个自变量的函数的函数。所以如果你有
    a -> b -> c
    

    那你有什么该函数接受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
    

    实际上是一个需要两件事的函数:m aa -> 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中创建所需的所有monadic类型。您不能做的是创建一个表示“此类型是monad类型”的想法的接口(interface)。您需要做的是:
    typeinterface Monad<M>
    {
      static <A>    M<A> Return(A a);
      static <A, B> M<B> Bind(M<A> m, Function<A, M<B>> f);
    }
    

    看看类型接口(interface)如何谈论泛型类型本身吗?一元类型是具有一个类型参数且具有这两种静态方法的通用M类型。但是您不能在Java或C#类型的系统中执行此操作。 Bind当然可以是将M<A>作为this的实例方法。但是除了静态之外,没有办法使Return成为任何东西。 Java无法让您(1)通过未构造的泛型类型对接口(interface)进行参数化,并且(2)无法指定静态成员是接口(interface)协定的一部分。

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



    好吧,您会这样想,但实际上却没有。首先,当然,任何具有足够类型系统的语言都可以定义单子(monad)类型。您可以在C#或Java中定义所需的所有monadic类型,只是不能说出它们在类型系统中的共同点。例如,您不能创建只能通过monadic类型进行参数化的泛型类。

    其次,您可以通过其他方式在语言中嵌入monad模式。 C#没有办法说“此类型与monad模式匹配”,但是C#的语言内置了查询理解(LINQ)。查询理解适用于任何单子(monad)类型!只是将绑定(bind)操作称为SelectMany,这有点奇怪。但是,如果您查看SelectMany的签名,则会发现它只是bind:
      static IEnumerable<R> SelectMany<S, R>(
        IEnumerable<S> source,
        Func<S, IEnumerable<R>> selector)
    

    这就是序列monad SelectManyIEnumerable<T>的实现,但是如果您编写的话,在C#中
    from x in a from y in b select z
    

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

    (上一段实际上是一个过分简化的谎言;出于性能原因,此查询使用的绑定(bind)模式与标准monadic绑定(bind)略有不同。从概念上讲,此模式识别为monad模式;实际上,细节略有不同。您有兴趣。)

    还有几点要点:

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



    好的选择;它通常称为“提取”操作。一个monad不必公开提取操作,但是bind当然必须能够从A中取出M<A>以便在其上调用Function<A, M<B>>,因此逻辑上通常存在某种提取操作。

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

    关于java - 为什么不能用Java声明Monad接口(interface)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35951818/

    相关文章:

    java - 如何为 RestController 启用 GZIP?

    java - Lucene 连字符词的索引/查询策略

    java - 电子邮件 Intent 问题。无法选择电子邮件选项

    haskell - 如何让 Reader 和 ReaderT 协同工作

    java - hibernate 空间 - 'Invalid endian flag value encountered' 异常

    scalac 在 org.squeryl.Table 中找不到正确的插入方法

    Swift Generic 在 typealias 中被视为父级

    c# - 类对泛型类型参数所做的事情的术语

    haskell - 将类型定义为 Monad

    f# - FSharpPlus : the fsi blocks when I try to combine two transformers