Haskell数据类型中的默认值

14

当您在面向对象的语言中定义类时,它通常会为成员变量设置默认值。在Haskell中是否有机制可以在记录类型中执行相同的操作?还有一个跟进问题:如果我们不从一开始就知道数据构造函数的所有值,而是通过IO交互获得它们,那么我们是否可以使用类似于OOP中的生成器模式构建类型?

提前感谢。


5
你无法在逐渐获得更多输入的过程中构建对象并逐步修改它,因为你不能修改任何东西。建造者模式没有太大用处:你只需进行所有的输入输出操作,在获得所需值后通过构造函数构建一个值即可。 - amalloy
3个回答

15

一个常见的习语是定义默认值。

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中。
如果您感到冒险,甚至可以使用更高级的方法静态分离“中间”选项值,这些值可能缺少一些字段,与“最终化”的选项值分开,后者必须具有所有字段。(不过我不建议Haskell初学者这样做。)

1
还有Default类型类,你可以使用DeriveAnyClassDeriveGeneric来派生它。顺便提一下,它将派生与defaultA相同的默认值。 - Alec
在我看来,当存在“好”的默认值时,它通常是一个“什么也不做”的值; 一个具有中性行为的值。这个描述符合 Data.Monoid 中的 mempty。在这个很好的答案中,AOptions 的默认值可以是标识值(如果这些类型都是幺半群的话,它们显然可以是)。 - Mark Seemann

9

在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 }

“something like the builder pattern from OOP?” 的意思是“类似于面向对象编程中的构造器模式吗?”
“我们可以使用一个State monad来实现。例如:”
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

现在我们可以使用以下代码创建一个版本为2RevisionList,最终列表为[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}

1
这里已经有很好的答案了,所以这个答案只是作为对chiWillem Van Onsem的优秀答案的补充。
在像Java和C#这样的主流面向对象语言中,默认对象不是未初始化的;相反,通常会使用其类型的默认值初始化默认对象,恰好对于引用类型,默认值是空引用。
Haskell没有空引用,因此记录不能用null初始化。最直接的对象翻译将是每个单一组成部分都是Maybe的记录。然而,这并不特别有用,但它突出了在OOP中保护不变量是多么困难。
生成器模式根本没有解决这个问题。任何生成器都必须从一个初始生成器对象开始,而该对象也将不得不具有默认值。

更多细节和大量示例,请参阅我关于此的文章系列。该文章系列专门关注测试数据生成器模式,但您应该能够看到它如何推广到通用的流畅构建器模式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接