Monad是继承层次结构的可行替代方法吗?


20

我将使用像monads这样的语言不可知的描述,首先描述monoid:

独异是(大约)的一组需要一些类型作为参数并返回相同的类型的功能。

单子是(大约)的一组需要的功能封装器类型作为参数并返回相同的包装类型。

注意这些是描述,而不是定义。随意攻击该描述!

因此,在OO语言中,monad允许以下操作组合:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

请注意,monad定义并控制这些操作的语义,而不是所包含的类。

传统上,在OO语言中,我们将使用类层次结构和继承来提供这些语义。因此,我们将有一个Bird带有方法takeOff()flyAround()和的类land(),而Duck将继承这些方法。

但是后来我们遇到了不能飞的鸟,因为penguin.takeOff()失败了。我们必须求助于异常抛出和处理。

另外,一旦我们说企鹅是a Bird,我们就会遇到多重继承问题,例如,如果我们也有的层次结构Swimmer

本质上,我们试图将类归类(对“分类论”的人道歉),并按类别而不是单个类来定义语义。但是,与层次结构相比,monad似乎是一种更清晰的机制。

因此,在这种情况下,我们将有一个Flier<T>单子,如上面的示例:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

...而且我们永远不会实例化一个Flier<Penguin>。我们甚至可以使用静态类型来防止这种情况的发生,也许使用标记接口。或运行时功能检查以纾困。但是,实际上,程序员切勿将企鹅放入Flier,从同样的意义上讲,它们切勿除以零。

而且,它更普遍适用。飞行器不必一定是鸟。例如Flier<Pterodactyl>Flier<Squirrel>,而不更改这些单个类型的语义。

一旦我们通过容器上的可组合函数对语义进行了分类(而不是按类型层次结构进行分类),它就解决了“某种行为,某种行为不适合”特定类的旧问题。它还轻松明确地为一个类提供了多种语义,例如Flier<Duck>Swimmer<Duck>。似乎我们一直在通过按类层次结构对行为进行分类来应对阻抗不匹配的问题。Monad优雅地处理它。

因此,我的问题是,就像我们倾向于偏爱组成而非继承一样,偏爱单子而不是继承是否也有意义?

(顺便说一句,我不确定是应该在这里还是在Comp Sci中使用,但这似乎更像是一个实际的建模问题。但是也许在那儿会更好。)


1
不确定我是否理解它的工作原理:松鼠和鸭子的飞行方式不同-因此“飞行动作”需要在这些类中实现...而飞行者需要一种方法来制造松鼠和鸭子飞行...也许在通用的Flier界面中...糟糕,请稍等...我错过了什么吗?
assylias 2014年

接口与类继承不同,因为接口定义功能,而功能继承定义实际行为。即使在“继承中的组成”中,定义接口仍然是重要的机制(例如,多态性)。接口不会遇到相同的多重继承问题。另外,每个传单可以(通过界面和多态性)提供功能属性,例如“ getFlightSpeed()”或“ getManuverability()”,以供容器使用。
罗布

3
您是否要问使用参数多态性是否总是替代亚型多态性的可行选择?
ChaosPandion 2014年

是的,随着添加可保留语义的可组合函数的皱折。参数化的容器类型已经存在很长时间了,但是就其本身而言,它们并不能完全解决问题。因此,这就是为什么我想知道单声道模式是否可以发挥更基本的作用。
罗布2014年

6
我不理解您对Monoid和Monad的描述。Monoid的关键特性是它涉及关联的二进制运算(请考虑浮点加法,整数乘法或字符串连接)。monad是一种抽象,它支持按某种顺序各种(可能是相关的)计算进行排序。
Rufflewind 2014年

Answers:


15

简短的答案是“ 否”,单子不能代替继承层次结构(也称为子类型多态性)。您似乎正在描述参量多态性,monad会利用它,但并非唯一这样做。

据我了解,单子从本质上讲与继承无关。我要说的是,这两个东西或多或少是正交的:它们旨在解决不同的问题,因此:

  1. 它们可以在至少两种意义上协同使用:
    • 查看Typeclassopedia,它涵盖了Haskell的许多类型类。您会注意到它们之间存在类似继承的关系。例如,Monad源自Applicative,而Applicative本身又来自Functor。
    • 作为Monad实例的数据类型可以参与类层次结构。记住,Monad更像是一个接口-为给定类型实现它可以告诉您有关数据类型的一些信息,但不是全部。
  2. 试图用一个做另一件事将是困难和丑陋的。

最后,尽管这与您的问题有关,但您可能有兴趣了解monad确实有一些难以置信的强大构图方式。阅读有关monad变压器的更多信息。但是,这仍然是一个活跃的研究领域,因为我们(而且我的意思是,比我聪明100000倍的人)还没有找到构成Monad的好方法,而且似乎有些Monad并不是随意编写的。


现在来提您的问题(对不起,我希望这对您有所帮助,而不是让您感到难过):我觉得我有很多可疑的前提,我会尝试阐明一些前提。

  1. monad是将容器类型作为参数并返回相同容器类型的一组函数。

    不,Monad在Haskell中:参数化类型m a,其实现为return :: a -> m a(>>=) :: m a -> (a -> m b) -> m b,并满足以下定律:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    有些Monad实例不是容器((->) b),有些容器不是(也不能做成)Monad实例(Set由于类型类的限制)。因此,“容器”的直觉是可怜的。见为更多的例子。

  2. 因此,在OO语言中,monad允许以下操作组合:

      Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
    

    一点都不。该示例不需要Monad。它所需要的只是具有匹配输入和输出类型的函数。这是另一种编写方法,强调了它只是函数应用程序:

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    我相信这是一种称为“流利接口”或“方法链接”的模式(但我不确定)。

  3. 请注意,monad定义并控制这些操作的语义,而不是所包含的类。

    也是monad的数据类型可以(并且几乎总是如此!)具有与monad无关的操作。这是一个Haskell示例,它由三个[]与monad没有任何关系的函数组成:[]“定义和控制操作的语义”,而“ contained class”没有,但这不足以构成monad:

    \predicate -> length . filter predicate . reverse
    
  4. 您已经正确地指出,使用类层次结构为事物建模存在一些问题。但是,您的示例并未提供任何证据表明monad可以:

    • 做好继承擅长的工作
    • 做好继承不善的工作

3
谢谢!我需要处理很多工作。我并不感到难过-我非常感谢您的见解。带着坏主意,我会感到更糟。:)(进入stackexchange的整个要点!)
Rob

1
@RobY不客气!顺便说一句,如果您以前从未听说过,我建议LYAH,因为它是学习monad(和Haskell!)的好资源,因为它有很多例子(我觉得做很多例子是最好的方法)。解决单子)。

这里有很多东西。我不想淹没这些评论,但有几条评论:#2 land(flyAround(takeOff(new Flier<Duck>(duck))))不起作用(至少在OO中如此),因为该构造需要破坏封装才能了解Flier的细节。通过在类上链接操作,Flier的细节保持隐藏,并且可以保留其语义。这类似于在Haskell中,monad绑定(a, M b)且不是绑定的原因,(M a, M b)因此monad不必将其状态公开给“ action”函数。
罗布2014年

#1,不幸的是,我试图模糊在Haskell中对Monad的严格定义,因为将任何内容映射到Haskell都有一个大问题:函数组成,包括构造函数的组成,使用Java这样的行人语言很难做到。因此,unit(大多数)成为包含类型的构造函数,并且bind(大多数)成为隐式的编译时操作(即早期绑定),该操作将“操作”功能与类联系在一起。如果您具有一流的函数或Function <A,Monad <B >>类,则bind方法可以进行后期绑定,但接下来将介绍这种滥用。;)
Rob

#3同意,这就是它的美。如果Flier<Thing>控制飞行的语义,那么它可以公开保持飞行语义的大量数据和操作,而“ monad”特定的语义实际上只是使其可链接和封装。这些担心可能不会(并且与我一直在使用的那些无关)不是monad内部类的担心:例如,Resource<String>具有httpStatus属性,而String没有。
罗布

1

因此,我的问题是,就像我们倾向于偏爱组成而非继承一样,偏爱单子而不是继承是否也有意义?

在非OO语言中,是的。在更传统的面向对象语言中,我会拒绝。

问题是大多数语言都没有类型专门化,这意味着您不能制作Flier<Squirrel>并且Flier<Bird>具有不同的实现。您必须做类似的事情static Flier Flier::Create(Squirrel)(然后为每种类型重载)。反过来,这意味着您每次添加新动物时都必须修改此类型,并且可能要复制大量代码才能使其起作用。

哦,而且在几种语言(例如C#)中public class Flier<T> : T {}是非法的。它甚至不会建立。大多数(如果不是全部)OO程序员期望Flier<Bird>仍然是Bird


感谢您的评论。我还有更多想法,但即使Flier<Bird>是一个参数化的容器,也只是琐碎的事,没有人会认为它是Bird(!?)List<String>是列表而不是字符串。
罗布2014年

@RobY- Flier只是一个容器。如果您仅将其视为容器,那么您为何会认为它可以代替继承的使用?
Telastyn 2014年

我在那里迷路了……我的意思是monad是增强的容器。Animal / Bird / Penguin通常是一个不好的例子,因为它带来了各种各样的语义。一个实际的例子是我们正在使用的REST式monad:Resource<String>.from(uri).get() ResourceString(或其他类型)的顶部添加语义,因此显然不是String
罗布2014年

@RobY-但它也与继承无关。
Telastyn 2014年

除了那是另一种类型的遏制。我可以将String放入Resource中,也可以抽象一个ResourceString类并使用继承。我的想法是,将类放到链接容器中比将其放到带有继承的类层次结构中是更好的抽象行为方式。因此,从“取代/消除”的意义上说“没有关联”是的。
罗布
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.