开始阅读之前:这个问题不是关于理解monad的问题,而是关于确定Java类型系统的局限性的,该局限性阻止了Monad
接口的声明。
在理解单子的过程中,我阅读了Eric Lippert的this SO-answer,该问题回答了有关单子的简单解释。在那里,他还列出了可以在monad上执行的操作:
有一种方法可以将未放大类型的值转换为放大类型的值。
有一种方法可以将未放大类型的操作转换为遵循前面提到的功能组成规则的放大类型的操作
通常,有一种方法可以使未放大类型从放大类型中退回。 (对于monad来说,这并非绝对必要,但通常存在这种操作。)
在阅读了有关monad的更多信息之后,我将第一个操作标识为return
函数,将第二个操作标识为bind
函数。我找不到第三项操作的常用名称,因此我将其称为unbox
函数。
为了更好地理解monad,我继续尝试在Java中声明一个通用的Monad
接口。为此,我首先查看了上面三个功能的签名。对于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
的M
接口的实现,则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
接口的另一种实现作为类型参数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>>
类型。这是用参数声明接口的第一次尝试:
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
。让我们尝试通过将?
重命名为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
类型?我找不到有关此的信息。我只找到有关声明混凝土单子的信息,例如Maybe
单子。
我有想念吗?我可以使用Java类型系统正确解决这些问题之一吗?如果我不能用Java类型系统解决问题2,那么Java是否有理由不警告我关于不可实例化的类型声明?
如前所述,这个问题不是关于理解单子的。如果我对单子的理解是错误的,您可能会对此有所提示,但不要尝试给出解释。如果我对单子的理解是错误的,那么所描述的问题仍然存在。
这个问题也不是关于是否可以在Java中声明Monad
接口。这个问题已经由Eric Lippert在上面链接的SO-answer中得到了答案:并非如此。这个问题是关于限制我执行此操作的限制到底是什么。埃里克·利珀特(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
和IFrob
以及List<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 x
和list y
是类型,而list
是一个更高的东西。比类型;它可以产生类型。所以说Java有3个层次,而Haskell有4个层次实际上更准确。不过,重点仍然是:Haskell有一个描述在a上可用的操作的概念。类型比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
必须具有两个操作:bind
和return
。这些操作的签名是什么?让我们先来看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 a
和一个a -> 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类型”的想法的接口。您需要做的是:
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>
作为this
的实例方法。但是除了静态以外,没有任何方法可以使Return
成为静态内容。 Java无法让您(1)通过未构造的泛型类型对接口进行参数化,并且(2)无法指定静态成员是接口协定的一部分。
由于存在适用于monad的语言,因此这些语言必须以某种方式声明Monad类型。
好吧,您会这样想,但实际上却没有。首先,当然,任何具有足够类型系统的语言都可以定义单子类型。您可以在C#或Java中定义所需的所有monadic类型,只是不能说出它们在类型系统中的共同点。例如,您不能创建只能通过monadic类型进行参数化的泛型类。
其次,您可以通过其他方式在语言中嵌入monad模式。 C#无法说“此类型与monad模式匹配”,但是C#具有内置于该语言中的查询理解(LINQ)。查询理解适用于任何单子类型!只是将绑定操作称为SelectMany
,这有点奇怪。但是,如果您查看SelectMany
的签名,就会发现它只是bind
:
static IEnumerable<R> SelectMany<S, R>(
IEnumerable<S> source,
Func<S, IEnumerable<R>> selector)
这是序列monad SelectMany
的IEnumerable<T>
的实现,但是在C#中,如果您编写
from x in a from y in b select z
那么a
的类型可以是任何单子类型,而不仅仅是IEnumerable<T>
。所需要的是a
是M<A>
,b
是M<B>
,并且在monad模式之后有一个合适的SelectMany
。因此,这是在语言中嵌入“ monad识别器”而不直接在类型系统中表示的另一种方法。
(上一段实际上是一个过分简化的谎言;出于性能原因,此查询使用的绑定模式与标准monadic绑定略有不同。从概念上讲,这可以识别monad模式;实际上,细节略有不同。请在此处阅读有关的详细信息如果您有兴趣。)
还有几点要点:
我找不到第三项操作的常用名称,因此我将其称为unbox函数。
好的选择;它通常称为“提取”操作。一个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)
当我写下面的代码时,编译器说 无法从ArrayList<String>转换为List<Comparable>private List<Comparable> get(){ return new ArrayList<String>(); } 但是当我用通配符编写返回类型时,代码会编译。private List&l…
合并List <T>和List <Optional <T >> - java鉴于: List<Integer> integers = new ArrayList<>(Arrays.asList( 10, 12 )); List<Optional<Integer>> optionalIntegers = Arrays.asList( Optional.of(5), Optional.em…
实例化类型<?>的泛型类 - java我正在为SCJP / OCPJP学习,并且遇到了一个对我来说很奇怪的示例问题。该示例代码实例化了两个通用集合:List<?> list = new ArrayList<?>(); List<? extends Object> list2 = new ArrayList<? extends Object>(); …
List <Dog>是List <Animal>的子类吗?为什么Java泛型不是隐式多态的? - java我对Java泛型如何处理继承/多态感到困惑。假设以下层次结构-动物(父母)狗-猫(儿童)因此,假设我有一个方法doSomething(List<Animal> animals)。根据继承和多态性的所有规则,我假设List<Dog>是List<Animal>,而List<Cat>是List<Animal&g…
展平地图中的列表列表 - java我有订单流(来源是订单列表)。每个订单都有一个客户和一个OrderLine列表。我要实现的目标是在一张简单的列表中以客户为关键的地图,并将属于该客户的所有订单行作为值。现在,通过执行以下操作,我现在管理的内容返回了Map<Customer>, List<Set<OrderLine>>>:orders .collect…