Haskell中数据类型设计和使用的最佳实践

时间:2014-01-03 06:48:07

标签: haskell

我的问题与Haskell程序设计的more general question有关。但我想专注于一个特定的用例。

我定义了一种数据类型(例如Foo),并通过模式匹配在函数(例如f)中使用它。后来,我意识到类型(Foo)需要一些额外的字段来支持新的功能。但是,添加字段会改变类型的使用方式;即,可能会影响取决于类型的现有功能。在现有代码中添加新功能,但不具吸引力,很难避免。我想知道Haskell语言级别的最佳实践是什么,以尽量减少这种修改的影响。

例如,现有代码是:

data Foo = Foo {
  vv :: [Int]
}

f :: Foo -> Int
f (Foo v) = sum v

如果我向f添加另一个字段,则函数Foo将出现语法错误:

data Foo = Foo {
  vv :: [Int]
  uu :: [Int]
}

但是,如果我首先将函数f定义为:

f :: Foo -> Int
f foo = sum $ vv foo

,即使对Foo进行了修改,f仍然是正确的。

3 个答案:

答案 0 :(得分:6)

镜头很好地解决了这个问题。只需定义一个指向感兴趣领域的镜头:

import Control.Lens

newtype Foo = Foo [Int]

v :: Lens' Foo [Int]
v k (Foo x) = fmap Foo (k x)

您可以将此镜头用作吸气剂:

view v :: Foo -> [Int]

......一个二传手:

set v :: [Int] -> Foo -> Foo

...和一个映射器:

over v :: ([Int] -> [Int]) -> Foo -> Foo

最好的部分是,如果您以后更改数据类型的内部表示,您所要做的就是将v的实现更改为指向感兴趣的字段的新位置。如果您的下游用户仅使用镜头与您的Foo进行互动,那么您将不会破坏向后兼容性。

答案 1 :(得分:2)

处理可能会在现有代码中添加要忽略的新字段的类型的最佳做法确实是使用记录选择器。

我想说你应该总是定义任何可能使用记录表示法改变的类型,并且你永远不应该使用带有位置参数的第一个样式对使用记录表示法定义的类型进行模式匹配。

表达上述代码的另一种方式是:

f :: Foo -> Int
f (Foo { vv = v }) = sum v

这可以说更优雅,在Foo有多个数据构造函数的情况下也更好。

答案 2 :(得分:1)

您的f函数非常简单,最简单的答案可能是使用合成以无点样式编写它:

f' :: Foo -> Int
f' = sum . vv

如果您的函数需要Foo值中的多个字段,则上述操作无效。但我们可以为Applicative使用(->)实例并执行以下操作:

import Control.Applicative

data Foo2 = Foo2 {
    vv' :: [Int]
  , uu' :: [Int]
  }

f2 :: Foo2 -> Int
f2 = sum . liftA2 (++) vv' uu' 

对于函数,liftA2将输入参数应用于两个函数,然后将结果组合到另一个函数(++)中。但也许这与晦涩难懂有关。