具有默认字段且需要与之配合使用的函数的数据类型

data type with a default field and that needs a function that works with it

说,我有一个数据类型

data FooBar a = Foo String Char [a]
              | Bar String Int [a]

我需要创建这种类型的值并将空列表作为第二个字段:

Foo "hello" 'a' []

Bar "world" 1 []

1) 我在我的代码中到处都这样做,我认为如果我能以某种方式省略空列表部分并隐式分配空列表会很好。这可能吗?类似于其他语言中的默认函数参数。

2) 由于这个 [] "default" 值,我经常需要有一个部分构造函数应用程序,它产生一个采用前两个值的函数:

mkFoo x y = Foo x y []
mkBar x y = Bar x y []

是否有 "better"(更惯用等)方法来做到这一点?避免定义新函数?

3) 我需要一种方法来向列表中添加内容:

add (Foo u v xs) x = Foo u v (x:xs)
add (Bar u v xs) x = Bar u v (x:xs)

这是惯用的做法吗?只是一个通用功能?

如您所见,我是初学者,所以这些问题可能意义不大。希望不会。

我会一一解答你的问题。

  1. Haskell 中不存在默认参数。它们根本不值得增加复杂性和组合损失。作为一种函数式语言,您在 Haskell 中进行了更多的函数操作,因此像默认参数这样的时髦性将很难处理。

  2. 我开始时没有意识到的一件事 Haskell 是数据构造函数就像其他一切一样都是函数。在您的示例中,

    Foo :: String -> Char -> [a] -> FooBar a
    

    因此你可以编写函数来填充其他函数的各种参数,然后这些函数将与 Foo 或 Bar 或其他任何东西一起工作。

    fill1 :: a -> (a -> b) -> b
    fill1 a f = f a
    --Note that fill1 = flip ($)
    
    fill2 :: b -> (a -> b -> c) -> (a -> c)
    --Equivalently, fill2 :: b -> (a -> b -> c) -> a -> c
    fill2 b f = \a -> f a b
    
    fill3 :: c -> (a -> b -> c -> d) -> (a -> b -> d)
    fill3 c f = \a b -> f a b c
    
    fill3Empty :: (a -> b -> [c] -> d) -> (a -> b -> d)
    fill3Empty f = fill3 [] f
    
    --Now, we can write 
    > fill3Empty Foo x y 
        Foo x y []
    
  3. lens 包为此类问题提供了优雅的解决方案。但是,一眼就能看出这个包非常复杂。这是您如何调用镜头包的最终结果:

    _list :: Lens (FooBar a) (FooBar b) [a] [b]
    _list = lens getter setter
      where getter (Foo _ _ as) = as
            getter (Bar _ _ as) = as
            setter (Foo s c _) bs = Foo s c bs
            setter (Bar s i _) bs = Bar s i bs
    

    现在我们可以做

    > over _list (3:) (Foo "ab" 'c' [2,1]) 
        Foo "ab" 'c' [3,2,1]
    

    一些解释:lens 函数在给定 getter 和某个类型的 setter 时生成 Lens 类型。 Lens s t a b 是一种表示“s 持有 at 持有 b 的类型。因此,如果你给我一个函数 a -> b ,我可以给你一个函数s -> t”。这正是 over 所做的:你为它提供一个镜头和一个函数(在我们的例子中,(3:) 是一个将 3 添加到列表前面的函数)并且它应用函数 "where the lens indicates".这与仿函数非常相似,但是,我们有更多的自由(在这个例子中,仿函数实例将有义务更改列表的每个元素,而不是对列表本身进行操作)。

    请注意,我们的新 _list 镜头非常通用:它在 FooBar 上同样有效,并且镜头包提供了 over 以外的许多功能来完成神奇的事情。

惯用的做法是获取函数或构造函数的那些您通常想要部分应用的参数,并将它们移到开头:

data FooBar a = Foo [a] String Char
              | Bar [a] String Int

foo :: String -> Char -> FooBar a
foo = Foo []

bar :: String -> Int -> FooBar a
bar = Bar []

类似地,将参数重新排序为 add 可以让您部分应用 add 以获得类型 FooBar a -> FooBar a 的函数,这很容易组合:

add :: a -> FooBar a -> FooBar a
add x (Foo xs u v) = Foo (x:xs) u v

add123 :: FooBar Int -> FooBar Int
add123 = add 1 . add 2 . add 3

add123 (foo "bar" 42) == Foo [1, 2, 3] "bar" 42

(2) 和 (3) 是做这些事情的完全正常和惯用的方式。特别是关于 (2),您偶尔会听到的一种表达方式是 "smart constructor"。这只是意味着像您的 mkFoo/mkBar 这样的函数会产生 FooBar a (或 Maybe (FooBar a) 等),并带有一些额外的逻辑以确保只能构造合理的值。

这里有一些可能(或可能没有!)有意义的技巧,具体取决于您尝试使用 FooBar 做什么。

如果您大多数时候以相似的方式使用 Foo 值和 Bar 值(即 Char 字段和 Int 字段之间的区别是一个小细节),排除相似之处并使用单个构造函数是有意义的:

data FooBar a = FooBar String FooBarTag [a]
data FooBarTag = Foo Char | Bar Int

除了在您不关心 FooBarTag 时避免案例分析之外,还允许您安全地使用记录语法(具有多个构造函数的记录和类型不能很好地混合)。

data FooBar a = FooBar
    { fooBarName :: String
    , fooBarTag :: FooBarTag
    , fooBarList :: [a]
    }

记录允许您使用字段而不必对整个内容进行模式匹配。

如果 FooBar 中的 所有 字段都有合理的默认值,您可以超越 mkFoo 类构造函数并定义默认值.

defaultFooBar :: FooBar a
defaultFooBar = FooBar
    { fooBarName = ""
    , fooBarTag = Bar 0
    , fooBarList = []
    }

您不需要记录来使用默认值,但它们允许方便地覆盖默认字段。

myFooBar = defaultFooBar
    { fooBarTag = Foo 'x'
    }

如果您厌倦了一遍又一遍地输入默认值的长名称,请考虑 data-default 包:

instance Default (FooBar a) where
    def = defaultFooBar

myFooBar = def { fooBarTag = Foo 'x' }

请注意,有相当数量的人 do not like the Default class,这并非没有道理。尽管如此,对于非常特定于您的应用程序的类型(例如配置设置),Default IMO 非常好。

最后,更新记录字段可能会很麻烦。如果您对此感到恼火,您会发现 lens 非常有用。请注意,它是一个库,对于初学者来说可能有点不知所措,所以事先深吸一口气。这是一个小样本:

{-# LANGUAGE TemplateHaskell #-} -- At the top of the file. Needed for makeLenses.
import Control.Lens

-- Note the underscores.
-- If you are going to use lenses, it is sensible not to export the field names.
data FooBar a = FooBar
    { _fooBarName :: String
    , _fooBarTag :: FooBarTag
    , _fooBarList :: [a]
    }
makeLenses ''FooBar -- Defines lenses for the fields automatically. 

defaultFooBar :: FooBar a
defaultFooBar = FooBar
    { _fooBarName = ""
    , _fooBarTag = Bar 0
    , _fooBarList = []
    }

-- Using a lens (fooBarTag) to set a field without record syntax.
-- Note the lack of underscores in the name of the lens.
myFooBar = set fooBarTag (Foo 'x') defaultFooBar

-- Using a lens to access a field.
myTag = view fooBarTag myFooBar -- Results in Foo 'x'

-- Using a lens (fooBarList) to modify a field.
add :: a -> FooBar a -> FooBar a
add x fb = over fooBarList (x :) fb

-- set, view and over have operator equivalents, (.~). (^.) and (%~) respectively.
-- Note that (^.) is flipped with respect to view.

这里有一个 gentle introductionlens 重点关注我在这里没有展示的方面,特别是镜头的组合有多好。