当您在面向对象的语言中定义类时,它通常会为成员变量设置默认值。在Haskell中是否有机制可以在记录类型中执行相同的操作?还有一个跟进问题:如果我们不从一开始就知道数据构造函数的所有值,而是通过IO交互获得它们,那么我们是否可以使用类似于OOP中的生成器模式构建类型?
提前感谢。
一个常见的习语是定义默认值。
data A = A { foo :: Int , bar :: String }
defaultA :: A
defaultA = A{foo = 0, bar = ""}
这样以后就可以用真实的数值“更新”它了。
doSomething :: Bool -> A
doSomething True = defaultA{foo = 32}
doSomething False = defaultA{bar = "hello!"}
伪代码示例:
data Options = O{ textColor :: Bool, textSize :: Int, ... }
defaultOptions :: Options
defaultOptions = O{...}
doStuff :: Options -> IO ()
doStuff opt = ...
main :: IO ()
main = do
...
-- B&W, but use default text size
doStuff defaultOptions{ color = False }
如果没有合理的默认值,可以将字段值包装在Maybe
中。Default
类型类,你可以使用DeriveAnyClass
和DeriveGeneric
来派生它。顺便提一下,它将派生与defaultA
相同的默认值。 - Alecmempty
。在这个很好的答案中,A
和 Options
的默认值可以是标识值(如果这些类型都是幺半群的话,它们显然可以是)。 - Mark Seemann在Haskell中,是否有机制可以在记录类型中执行相同的操作?
你可以做的是隐藏构造函数,并提供一个函数作为构造函数。
举个例子,我们有一个要更新的列表,还有一个修订号,那么我们可以定义它为:
data RevisionList a = RevisionList { theList :: [a],
revision :: Int }
deriving Show
BuildList
:revisionList :: [a] -> RevisionList a
revisionList xs = RevisionList { theList = xs, revision=0 }
通过将构造函数隐藏在module
导出中,我们因此隐藏了使用除版本0
之外的其他版本进行初始化的可能性。因此,该模块可能如下所示:
module Foo(RevisionList(), revisionList)
data RevisionList a = RevisionList { theList :: [a],
revision :: Int }
revisionList :: [a] -> RevisionList a
revisionList xs = RevisionList { theList = xs, revision=0 }
module Foo(RevisionList(), revisionList,
increvision, RevisionListBuilder, prefixList)
import Control.Monad.State.Lazy
type RevisionListBuilder a = State (RevisionList a)
increvision :: RevisionListBuilder a ()
increvision = do
rl <- get
put (rl { revision = 1 + revision rl})
prefixList :: a -> RevisionListBuilder a ()
prefixList x = do
rl <- get
put (rl { theList = x : theList rl })
increvision
因此,我们目前已经获取了RevisionList
,执行更新,将新结果put
回去,并增加修订号。
现在,另一个模块可以导入我们的Foo
,并像这样使用构建器:
some_building :: RevisionListBuilder Int ()
some_building = do
prefixList 4
prefixList 1
现在我们可以使用以下代码创建一个版本为2
的RevisionList
,最终列表为[1,4,2,5]
:
import Control.Monad.State.Lazy(execState)
some_rev_list :: RevisionList Int
some_rev_list = execState some_building (revisionList [2,5])
因此,它大致看起来像:
Foo.hs
:
module Foo(RevisionList(), revisionList,
increvision, RevisionListBuilder, prefixList)
data RevisionList a = RevisionList { theList :: [a],
revision :: Int }
deriving Show
type RevisionListBuilder a = State (RevisionList a)
revisionList :: [a] -> RevisionList a
revisionList xs = RevisionList { theList = xs, revision=0 }
increvision :: RevisionListBuilder a ()
increvision = do
rl <- get
put (rl { revision = 1 + revision rl})
prefixList :: a -> RevisionListBuilder a ()
prefixList x = do
rl <- get
put (rl { theList = x : theList rl })
increvision
Bar.hs
:
import Foo
import Control.Monad.State.Lazy(execState)
some_building :: RevisionListBuilder Int ()
some_building = do
prefixList 4
prefixList 1
some_rev_list :: RevisionList Int
some_rev_list = execState some_building (revisionList [2,5])
现在我们已经通过建造some_building
来构建了一个反转列表some_rev_list
:
Foo Bar> some_rev_list
RevisionList {theList = [1,4,2,5], revision = 2}
Maybe
的记录。然而,这并不特别有用,但它突出了在OOP中保护不变量是多么困难。更多细节和大量示例,请参阅我关于此的文章系列。该文章系列专门关注测试数据生成器模式,但您应该能够看到它如何推广到通用的流畅构建器模式。