【问题标题】:Default values in Haskell data typesHaskell 数据类型中的默认值
【发布时间】:2020-11-12 10:06:35
【问题描述】:

当您在面向对象的语言中定义一个类时,它通常会为成员变量设置默认值。 Haskell 中是否有任何机制可以在记录类型中做同样的事情?还有一个后续问题:如果我们从一开始就不知道数据构造函数的所有值,而是从 IO 交互中获取它们,我们是否可以使用类似于 OOP 中的构建器模式的东西来构建类型?

提前致谢

【问题讨论】:

  • 你不能构建一个对象并随着输入的增加逐渐修改它,因为你不能修改任何东西。构建器模式没有太多用处:您只需执行所有 IO,然后一旦获得所需的值,您就可以通过构造器构建一个值。

标签: haskell types


【解决方案1】:

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

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 中。

如果您喜欢冒险,您甚至可以使用more advanced approach 将可能缺少一些字段的“中间”选项值与必须包含所有字段的“最终”选项值静态分开。 (不过,我不会向 Haskell 初学者推荐这个。)

【讨论】:

  • 还有Default 类型类,您可以使用DeriveAnyClassDeriveGeneric 派生它。顺便说一句,它会派生出与defaultA 相同的默认值。
  • 在我看来,当一个“好”的默认值存在时,它通常是一个“什么都不做”的值;具有中性行为的值。此描述适合来自Data.MonoidmemptyAOptions 在这个很好的答案中的默认值可能是标识值,如果这些类型是幺半群,它们可能是微不足道的。
【解决方案2】:

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 }

类似于 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

所以到目前为止,我们 get 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}

【讨论】:

    【解决方案3】:

    这里已经有了很好的答案,所以这个答案只是作为对 chiWillem Van Onsem 优秀答案的补充。

    在Java和C#等主流面向对象语言中,并不是默认对象是未初始化的;相反,默认对象通常使用其类型的默认值初始化,而对于引用类型,默认值是空引用。

    Haskell 没有空引用,因此不能用空值初始化记录。最直接的对象翻译是每个组成元素都是Maybe 的记录。然而,这并不是特别有用,但它强调了在 OOP 中保护不变量是多么困难。

    建造者模式根本没有解决这个问题。任何 Builder 都必须从初始 Builder 对象开始,并且该对象也必须具有默认值。

    有关更多详细信息和大量示例,我写了一个article series about this。本系列文章特别关注 Test Data Builder 模式,但您应该能够看到它是如何概括为 Fluent Builder 模式的。

    【讨论】:

      猜你喜欢
      • 2015-05-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-11
      • 1970-01-01
      • 2012-05-23
      相关资源
      最近更新 更多