当前位置: 首页 > 面试题库 >

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

轩辕海
2023-03-14
问题内容

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

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

  1. 有一种方法可以将未放大类型的值转换为放大类型的值。
  2. 有一种方法可以将未放大类型的操作转换为遵循前面提到的功能组成规则的放大类型的操作
  3. 通常,有一种方法可以使非放大类型从放大类型中恢复出来。(对于单子来说,这并非是绝对必要的,但通常存在这种操作。)

在阅读了有关monad的更多信息之后,我将第一个操作确定为return函数,将第二个操作确定为bind函数。我找不到第三次操作的常用名称,因此我将其称为unbox函数。

为了更好地理解monad,我继续尝试Monad用Java 声明一个通用接口。为此,我首先看了上面三个功能的签名。对于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。因此,如果我有一个Monad名为的接口的实现MM.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接口的另一种实现作为type参数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<>();

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

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

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

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

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

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

现在,我们遇到了另一个问题。我们在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);
}

引入另一个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类型?我找不到有关此的信息。我只找到有关声明单子像Maybe单子的信息。

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

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

这个问题也不是关于是否可以Monad用Java 声明接口。这个问题已经由Eric Lippert在他上方的SO-
答案中得到了答案:并非如此。这个问题是关于阻止我执行此操作的限制到底是什么。埃里克·利珀特(Eric Lippert)将其称为高级类型,但我无法直视它们。

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


问题答案:

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

好问题!

埃里克·利珀特(Eric Lippert)将其称为高级类型,但我无法直视它们。

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

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

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

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

在这里,让我简化一下。Haskell让您声明自己的infix运算符进行绑定,但是我们将其称为bind:

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

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

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

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

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

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

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

(旁白:我想指出的是,我有点过分简化了。例如List<int>,以Java
和Java为例List<String>。这是两个“类型”,但Java认为它们是一个“类”,因此从某种意义上讲Java也它的类比类型“高”,但是再说一遍,您可以在Haskell中说list xlist y类型,list这是比类型高的东西;它是可以产生类型的东西。实际上,说Java有 3个 级别,而Haskell有 4
级别,则更准确地说,不过,重点仍然是: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<A>。也就是说,这种手段m是一个通用型,a是一种类型,m am参数化用a

x -> yHaskell中的语法是“具有类型x并返回类型的函数y”。是Function<X, Y>

放在一起,我们就有return了一个函数,该函数接受type的参数a并返回type 的值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

实际上是一个需要两件事的函数:an m a和a a -> m b并返回an 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类型”想法的接口。您需要做的是:

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<A>as
的实例方法this。但是Return除了静态之外,别无他法。Java无法让您(1)通过未 构造的
泛型类型对接口进行参数化,并且(2)无法指定静态成员是接口协定的一部分。

由于存在适用于monad的语言,因此这些语言必须以某种方式声明Monad类型。

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

其次,您可以通过其他方式在语言中嵌入monad模式。C#无法说“此类型与monad模式匹配”,但是C#具有内置于该语言中的查询理解(LINQ)。查询理解适用于任何单子类型!只是必须调用bind操作SelectMany,这有点奇怪。但是,如果您看一下的签名SelectMany,就会发现它只是bind

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

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

from x in a from y in b select z

然后a的类型可以是 任何
单子类型,而不仅仅是IEnumerable<T>。所需要的是该aM<A>,即bM<B>,并且有合适的
SelectMany随后的单子图案。因此,这是在语言中嵌入“ monad识别器”而不直接在类型系统中表示的另一种方法。

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

还有几点要点:

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

好的选择; 它通常称为“提取”操作。一个 monad
不必公开提取操作,但是当然bind需要某种方式才能A退出M<A>调用Function<A, M<B>>它,因此在逻辑上通常存在某种提取操作。

一个 共鸣 -从某种意义上说是向后的单子-
要求extract公开操作;extract本质上是return倒退的。同样,comonad也需要进行extend某种bind后退的操作。有签名static M<B> Extend(M<A> m, Func<M<A>, B> f)



 类似资料:
  • 问题内容: 有时我们有几个类,这些类的某些方法具有相同的签名,但是与声明的Java接口不对应。例如,和(在中的其他几个 )中都有一个方法 现在,假设我希望对具有该方法的对象进行一些操作。然后,我想有一个接口(或者自己定义),例如 这样我可以写: 但是,可悲的是,我不能: 此演员表将是非法的。编译器 知道 这 是不是 一个,因为类没有宣布实现该接口...... 然而“实际上”实现它 。 有时这会带来

  • 问题内容: 我试图找到为什么不能将类创建为静态类的原因?喜欢: 问题答案: 在Java中,关键字通常将一个方法或字段标记为不存在,而不是每个类实例一次,而是一次。一个类一旦存在就已经存在,因此实际上,所有类都以这种方式是“静态的”,并且所有对象都是该类的实例。 确实对 内部 类具有含义,这是完全不同的:通常,内部类实例可以访问与其绑定的外部类实例的成员,但是如果内部类为,则它没有这样的引用并且可以

  • 我习惯像这样声明数组内联: 为什么我不能对函数执行相同的操作?假设我有一个类,其中包含and 方法,这是有效的: 但是,这不是: 它不喜欢内联数组声明,编译器错误是“不能创建函数的泛型数组” 编辑 我认为我的问题不是建议的副本,因为我想使用数组初始值设定项语法静态定义一组函数

  • 问题内容: 该主题充分说明了这一点-为什么不能在接口中声明静态方法的原因是什么? 上面的代码给了我以下错误(至少在Eclipse中):“接口方法ITest.test()的非法修饰符;仅允许public&abstract”。 问题答案: 这里有一些问题。第一个问题是声明静态方法而不定义它的问题。这是之间的区别 和 由于Espo提到的原因,第一个是不可能的:你不知道哪个实现类是正确的定义。 Java

  • 问题内容: 为什么不能在Java中将类声明为静态类? 问题答案: 只有嵌套的类可以是静态的。这样,你可以使用嵌套类而无需外部类的实例。

  • 问题内容: 将接口声明为抽象有什么意义?接口方法也是如此。有什么意义吗? 例如。 问题答案: 您在哪里遇到过已发布的代码块,任何旧的Java代码库? 这就是 JLS不得不说: 9.1.1.1抽象接口: 每个接口都是隐式抽象的。该修饰符已过时,不应在新程序中使用。 9.4抽象方法声明: 为了与Java平台的较早版本兼容,出于风格考虑,允许但不鼓励为接口中声明的方法冗余地指定abstract修饰符。