在Haskell中设计和使用数据类型的最佳实践
我的问题涉及到关于Haskell程序设计的更一般的问题。 但我想关注一个特定的用例。
我定义了一个数据类型(例如Foo
),并通过模式匹配将它用于一个函数(例如f
)中。 后来,我意识到类型( Foo
)需要一些额外的字段来支持新的功能。 但是,添加该字段会改变该类型的使用方式; 即取决于类型的现有功能可能受到影响。 为现有代码添加新功能,但无法吸引,难以避免。 我想知道Haskell语言级别的最佳实践是什么,以最大限度地减少这种修改的影响。
例如,现有的代码是:
data Foo = Foo {
vv :: [Int]
}
f :: Foo -> Int
f (Foo v) = sum v
如果我将另一个字段添加到Foo
,函数f
将会是语法错误:
data Foo = Foo {
vv :: [Int]
uu :: [Int]
}
但是,如果我首先将函数f
定义为以下内容:
f :: Foo -> Int
f foo = sum $ vv foo
,那么即使对Foo
进行了修改, f
仍然是正确的。
镜头很好地解决了这个问题。 只需定义一个指向感兴趣的领域的镜头:
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
进行交互,那么您不会破坏向后兼容性。
处理可能获得新字段的类型的最佳实践添加了您想要在现有代码中忽略的事实的确如您所做的那样使用记录选择器。
我会说你应该总是定义任何可能使用记录符号改变的类型,并且你绝不应该使用带位置参数的第一个样式在记录符号定义的类型上匹配类型。
表达上述代码的另一种方式是:
f :: Foo -> Int
f (Foo { vv = v }) = sum v
这可以说更加优雅,而且在Foo
有多个数据构造函数的情况下它也更好。
你的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
将输入参数应用于两个函数,然后将结果liftA2
到另一个函数中(++)
本例中为(++)
。 但也许这与边界不清晰有关。
上一篇: Best Practice on design and usage of data type in Haskell