为什么 Monad 接口不能用 Java 声明?在接口声明中使用具体类型在类型自己的声明中使用带有移位类型参数的类型总结一下

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


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

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

在阅读了有关monads的更多信息后,我将第一个操作确定为函数,将第二个操作确定为函数。我无法找到第三个操作的常用名称,因此我将仅将其称为函数。returnbindunbox

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

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

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

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

因此,接口仅包含函数。Monadbind

让我们尝试声明接口:

public interface Monad {
    Monad bind();
}

有两个缺陷:

  • 该函数应返回具体实现,但它仅返回接口类型。这是一个问题,因为我们在具体的子类型上声明了取消装箱操作。我将这称为问题1bind
  • 该函数应将函数作为参数检索。我们稍后将对此进行讨论。bind

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

这解决了问题1:如果我对monad的理解是正确的,那么该函数总是返回一个与它被调用的monad相同的具体类型的新monad。所以,如果我有一个名为 的接口的实现,那么将返回另一个但不是.我可以使用泛型来实现这一点:bindMonadMM.bindMMonad

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 */ }
}

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

  • 一旦一个实现类没有提供自身,而是提供接口的另一个实现作为类型参数,就会崩溃,因为这样方法将返回错误的类型。例如MonadMbind

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

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

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

    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<>();
    

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

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

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

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

我们必须将类型作为泛型类型参数添加到接口声明中,以便我们可以在函数签名中使用它。第一个是 类型 返回的 monad 。要将其替换为 ,我们必须将自身添加为泛型类型参数:T1?T1MT2T2

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

现在,我们遇到了另一个问题。我们在接口中添加了第三个类型参数,因此我们必须在它的用法中添加一个新的。我们将暂时忽略新的,以首先调查现在。它是类型 返回的 monad 。让我们尝试通过重命名和引入另一个来删除它:Monad???MM?MM1M2

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类型系统中缺少哪些功能?由于有些语言与monads一起工作,因此这些语言必须以某种方式声明类型。这些其他语言如何声明类型?我无法找到有关此内容的信息。我只找到有关具体monads声明的信息,比如monad。MonadMonadMaybe

我错过了什么吗?我能否正确解决 Java 类型系统的这些问题之一?如果我无法解决Java类型系统的问题2,那么Java为什么不警告我不可实例化的类型声明吗?


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

这个问题也不是关于是否可以在Java中声明接口。埃里克·利珀特(Eric Lippert)在上面链接的SO-answer中已经回答了这个问题:事实并非如此。这个问题是关于阻止我这样做的限制究竟是什么。埃里克·利珀特(Eric Lippert)将此称为高级类型,但我无法理解它们。Monad

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


答案 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中,我们有and和,它们都是类型。intIFrobList<IBar>

从这一点开始,抛弃你对长颈鹿是动物子类的任何直觉,依此类推;我们不需要它。想想一个没有遗产的世界;它不会再进入这个讨论。

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

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

(题外话:我想在这里指出,我有点过于简单化了。例如,考虑 Java 中的 和 。这是两种“类型”,但Java认为它们是一个“类”,所以从某种意义上说,Java也有比类型“更高”的类。但话又说回来,你可以在Haskell中说同样的话,那就是和类型,这是一个比类型更高的东西;这是一个可以产生类型的东西。因此,实际上更准确地说Java有个级别,Haskell有个级别。但重点仍然存在:Haskell有一个概念,即描述比Java更强大的类型上可用的操作。我们将在下面更详细地介绍这一点。List<int>List<String>list xlist ylist

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

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

class Monad m where

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

假设我们有一个类型是这个类的成员;叫它.要使该类型成为类的成员,必须对此类型执行哪些操作?mMonad

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

操作的名称位于 的左侧,签名位于右侧。因此,要成为 ,类型必须具有两个操作:和 。这些操作的签名是什么?让我们先来看看。::Monadmbindreturnreturn

  a -> m a

m a是 Haskell 在 Java 中会是什么。也就是说,这意味着是泛型类型,是类型,使用 参数化。M<A>mam ama

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

把它放在一起,我们有一个函数,它接受一个类型的参数并返回一个类型的值。或者在爪哇returnam a

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

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

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

a -> b -> c

那你得到了什么?一个函数,它接受 并返回 .因此,假设您想创建一个函数,该函数采用两个数字并返回其总和。您将创建一个函数,该函数采用第一个数字,并返回一个函数,该函数采用第二个数字并将其添加到第一个数字。ab -> 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

实际上是一个包含两个东西的函数:a 和 a,它返回一个 。或者,在Java中,它直接是:m aa -> m bm b

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中创建所需的所有一元类型。你不能做的是制作一个代表“此类型是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);
}

看看类型接口如何讨论泛型类型本身?一元类型是具有一个类型参数并具有这两个静态方法的泛型的任何类型。但是你不能在Java或C#类型的系统中做到这一点。 当然可以是一个实例方法,它采用 as .但是除了静态之外,没有办法制造任何东西。Java 无法 (1) 通过未构造的泛型类型参数化接口,并且 (2) 无法指定静态成员是接口协定的一部分。MBindM<A>thisReturn

由于有些语言与monads一起工作,这些语言必须以某种方式声明Monad类型。

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

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

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

这是序列 monad 的实现,但在 C# 中,如果您编写SelectManyIEnumerable<T>

from x in a from y in b select z

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

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

还有几个小问题:

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

不错的选择;它通常称为“提取”操作。monad不需要公开提取操作,但当然需要以某种方式能够从中获取,以便调用它,因此逻辑上通常存在某种提取操作。bindAM<A>Function<A, M<B>>

一个 comonad——从某种意义上说,一个向后 monad——需要一个操作来暴露; 基本上是倒退的。comonad也需要一种向后翻转的操作。它具有签名extractextractreturnextendbindstatic M<B> Extend(M<A> m, Func<M<A>, B> f)


答案 2

如果你看看AspectJ项目在做什么,它类似于将monads应用于Java。他们这样做的方式是后处理类的字节码以添加附加功能 - 他们必须这样做的原因是,如果没有AspectJ扩展,语言中就无法做他们需要做的事情;语言不够富有表现力。

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

本文描述了 AspectJ 中面向 Aspect 的编程如何形式化为 monads。

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


推荐