如何在Haskell中建模类层次结构?

Kib*_*rim 16 oop haskell functional-programming

我是C#开发人员.来自世界的OO方面,我首先考虑接口,类和类型层次结构.由于Haskell中缺少OO,有时我发现自己陷入困境,我无法想到用Haskell模拟某些问题的方法.

如何在Haskell中建模涉及类层次结构的真实世界情况,如下所示:http://www.braindelay.com/danielbray/endangered-object-oriented-programming/isHierarchy-4.gif

C. *_*ann 32

首先:标准OO设计在Haskell中不能很好地工作.你可以对抗这种语言并尝试制作类似的东西,但这将是一种令人沮丧的练习.因此,第一步是为您的问题寻找Haskell风格的解决方案,而不是寻找在Haskell中编写OOP风格解决方案的方法.

但说起来容易做起来难!哪里开始?

所以,让我们拆开OOP为我们做的事情的细节,并思考这些在Haskell中的表现.

  • 对象:粗略地说,对象是一些数据与对该数据进行操作的方法的组合.在Haskell中,数据通常使用代数数据类型构建; 方法可以被认为是将对象的数据作为初始隐式参数的函数.
  • 封装:但是,检查对象数据的能力通常仅限于自己的方法.在Haskell中,有多种隐藏数据的方法,两个例子是:
    • 在不导出类型构造函数的单独模块中定义数据类型.只有该模块中的函数可以检查或创建该类型的值.这有点类似protectedinternal成员.
    • 使用部分申请.考虑函数map及其参数被翻转.如果将它应用于Ints 列表,您将获得类型函数(Int -> b) -> [b].从某种意义上说,你给它的列表仍然是"那里",但除了通过函数之外没有别的东西可以使用它.这与private成员相当,并且部分应用的原始函数与OOP样式的构造函数相当.
  • "Ad-hoc"多态:通常,在OO编程中,我们只关心某些东西实现了一个方法; 当我们调用它时,调用的具体方法是根据实际类型确定的.Haskell 为编译时函数重载提供了类型类,它们在许多方面比OOP语言中的灵活.
  • 代码重用:老实说,我认为通过继承进行代码重用是错误的.像Ruby这样的混合物让我觉得它是一个更好的OO解决方案.无论如何,在任何函数式语言中,标准方法是使用高阶函数分解出常见行为,然后专门化通用形式.这里的一个典型示例是fold函数,它概括了几乎所有迭代循环,列表转换和线性递归函数.
  • 接口:根据您使用接口的方式,有不同的选项:
    • 要解耦实现:具有类型约束的多态函数是您想要的.例如,函数sort有类型(Ord a) => [a] -> [a]; 除了它必须是某种类型实现的列表之外,它与您提供的类型的细节完全分离Ord.
    • 使用共享接口处理多种类型:为此,您需要对存在类型进行语言扩展,或者为了保持简单,在上面使用部分应用程序的一些变体- 而不是可以应用于它们的值和函数,应用提前运作并使用结果.
  • Subtyping,又名"is-a"关系:这是你运气不好的地方.但是 - 从经验来看,多年来一直是专业的C#开发人员 - 你真正需要子类型的情况并不常见.相反,请考虑上述情况,以及您尝试使用子类型关系捕获的行为.

您可能还会发现此博文有用; 它简要概述了您在Haskell中使用的内容,以解决一些标准设计模式在OOP中经常使用的相同问题.

作为最终的附录,作为C#程序员,您可能会发现研究它与Haskell之间的联系很有意思.很多负责C#的人也是Haskell程序员,最近C#的一些新增内容深受Haskell的影响.最值得注意的可能是LINQ下的monadic结构,IEnumerable本质上是list monad.

  • @Jonathan Sterling:这些术语通常含糊不清或误导,特别是当神秘但精确的学术术语开始混淆描述性但模糊的行业术语时,所以很容易弄错.同样的事情发生在正式的数学概念和他们在Haskell中的手工波浪使用,这导致我在几个数学家面前尴尬! (3认同)

sep*_*p2k 13

让我们假设以下操作:人类可以说话,狗可以吠叫,如果物种的性别相反,物种的所有成员都可以与同一物种的成员交配.我会像这样在haskell中定义它:

data Gender = Male | Female deriving Eq

class Species s where
    gender :: s -> Gender

-- Returns true if s1 and s2 can conceive offspring
matable :: Species a => a -> a -> Bool
matable s1 s2 = gender s1 /= gender s2

data Human = Man | Woman
data Canine = Dog | Bitch

instance Species Human where
    gender Man = Male
    gender Woman = Female

instance Species Canine where
    gender Dog = Male
    gender Bitch = Female

bark Dog = "woof"
bark Bitch = "wow"

speak Man s = "The man says " ++ s
speak Woman s = "The woman says " ++ s
Run Code Online (Sandbox Code Playgroud)

现在操作matable有类型Species s => s -> s -> Bool,bark有类型Canine -> Stringspeak类型Human -> String -> String.

我不知道这是否有帮助,但考虑到问题的相当抽象性,这是我能想到的最好的.

编辑:回应丹尼尔的评论:

集合的简单层次结构可能如下所示(忽略现有的类,如Foldable和Functor):

class Foldable f where
    fold :: (a -> b -> a) -> a -> f b -> a

class Foldable m => Collection m where
    cmap :: (a -> b) -> m a -> m b
    cfilter :: (a -> Bool) -> m a -> m a

class Indexable i where
    atIndex :: i a -> Int -> a

instance Foldable [] where
    fold = foldl

instance Collection [] where
    cmap = map
    cfilter = filter

instance Indexable [] where
    atIndex = (!!)

sumOfEvenElements :: (Integral a, Collection c) => c a -> a
sumOfEvenElements c = fold (+) 0 (cfilter even c)
Run Code Online (Sandbox Code Playgroud)

现在sumOfEvenElements采用任何类型的积分集合,并返回该集合的所有偶数元素的总和.


Nor*_*sey 6

Haskell使用抽象数据类型而不是类和对象.这些是关于组织构建观察信息的方式的问题的两个兼容的观点.我所知道的关于这个主题的最好的帮助是William Cook的论文面向对象编程与抽象数据类型.他对这种效果有一些非常明确的解释

  • 在基于类的系统中,代码是围绕构造抽象的不同方式组织的.通常,构造抽象的每种不同方式都被赋予其自己的类.这些方法只知道如何观察该结构的属性.

  • 在基于ADT的系统(如Haskell)中,代码围绕观察抽象的不同方式进行组织.通常,观察抽象的每种不同方式都被赋予其自己的功能.该函数知道可以构造抽象的所有方法,并且它知道如何观察单个属性,但是知道任何构造.

Cook的论文将向您展示一个很好的抽象矩阵布局,并教您如何组织任何类作为ADY,反之亦然.

层次结构涉及另外一个元素:通过继承重用实现.在Haskell中,这种重用是通过第一类函数实现的:Primate抽象中的函数是一个值,Human抽象的实现可以重用抽象的任何函数Primate,可以包装它们来修改它们的结果,等等.

设计与类层次结构和设计与抽象数据类型之间并不完全吻合.如果你试图从一个音译到另一个,你将会遇到一些尴尬而不是惯用的东西 - 就像用Java编写的FORTRAN程序.但是,如果您了解类层次结构的原则和抽象数据类型的原则,您可以在一种风格中解决问题,并为另一种风格的同一问题制定合理的惯用解决方案.它确实需要练习.


附录:也可以使用Haskell的类型系统来尝试模拟类层次结构,但这是一个不同的鱼类.类型类与普通类相似,许多标准示例都有效,但它们不同,可能会有一些非常大的意外和不适应.虽然类型类是Haskell程序员的宝贵工具,但我建议任何学习Haskell的人都要学习使用抽象数据类型设计程序.