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

时间:2010-06-26 16:02:57

标签: oop functional-programming haskell

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

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

4 个答案:

答案 0 :(得分:32)

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

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

所以,让我们拆解OOP为我们做的事情的细节,并考虑一下Haskell的外观。

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

您可能还会发现this blog post有帮助;它简要概述了您在Haskell中使用的内容,以解决在OOP中经常使用某些标准设计模式的相同问题。

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

答案 1 :(得分: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

现在,操作matable的类型为Species s => s -> s -> Boolbark的类型为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)

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

答案 2 :(得分:6)

Haskell使用抽象数据类型而不是类和对象。这些是关于组织构建观察信息的方法的问题的两个兼容视图。关于这个问题,我所知道的最好的帮助是威廉库克的论文Object-Oriented Programming Versus Abstract Data Types。他对

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

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

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

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

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


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

答案 3 :(得分:2)

Haskell是我最喜欢的语言,是一种纯粹的功能语言。 它没有副作用,没有任务。 如果你发现很难过渡到这种语言,也许F#是一个更好的开始函数式编程的地方。 F#并不纯粹。

对象封装状态,有一种方法可以在Haskell中实现这一点,但这是需要花费更多时间学习的问题之一,因为你必须学习一些类别理论概念才能深入理解monad。有一种语法糖可以让你看到像非破坏性赋值的单子,但在我看来,花更多的时间来理解类别理论的基础(类别的概念)以获得更好的理解更好。

在尝试在Haskell中以OO风格编程之前,你应该问自己是否真的在C#中使用面向对象的风格,许多程序员使用OO语言,但他们的程序是用结构化风格编写的。

数据声明允许您定义组合产品(相当于C语言中的结构)和联合(相当于C中的union)的数据结构,声明的派生部分允许继承默认方法。

如果类中包含方法集的实现,则数据类型(数据结构)属于类。 例如,如果您可以定义show :: a - >您的数据类型的String方法,然后它属于Show类,您可以将您的数据类型定义为Show类的实例。

这与某些OO语言中类的使用不同,后者用它来定义结构+方法。

如果数据类型独立于其实现,则它是抽象的。您可以通过抽象接口创建,变异和销毁对象,您不需要知道它是如何实现的。

Haskell支持抽象,很容易声明。 例如,来自Haskell站点的代码:

data Tree a = Nil 
            | Node { left  :: Tree a,
                     value :: a,
                     right :: Tree a }

声明选择器left,value,right。 如果要将它们添加到模块声明中的导出列表中,可以将构造函数定义如下:

node = Node 
nil = Nil

模块的构建方式与Modula类似。以下是同一网站的另一个例子:

module Stack (Stack, empty, isEmpty, push, top, pop) where

empty :: Stack a
isEmpty :: Stack a -> Bool
push :: a -> Stack a -> Stack a
top :: Stack a -> a
pop :: Stack a -> (a,Stack a)

newtype Stack a = StackImpl [a] -- opaque!
empty = StackImpl []
isEmpty (StackImpl s) = null s
push x (StackImpl s) = StackImpl (x:s)
top (StackImpl s) = head s
pop (StackImpl (s:ss)) = (s,StackImpl ss)

关于这个主题还有更多的话要说,我希望这个评论有所帮助!