3

说,我有一个数据类型

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

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

Foo "hello" 'a' []

或者

Bar "world" 1 []

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

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

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

是否有“更好”(更惯用等)的方法来做到这一点?避免定义新功能?

3)我需要一种将东西添加到列表中的方法:

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

这是惯用的方式吗?只是一个通用功能?

如您所见,我是初学者,所以也许这些问题没有多大意义。希望不是。

4

3 回答 3

2

我会一一解答你的问题。

  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. 镜头包为此类问题提供了优雅的解决方案。但是,您一眼就能看出这个包非常复杂。这是您如何调用镜头包的最终结果:

    _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]
    

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

    请注意,我们的新 _list 镜头非常通用:它同样适用,Foo并且Bar镜头包提供了许多功能,而不是over用于做神奇的事情。

于 2015-07-29T16:45:22.483 回答
2

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

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使您可以部分应用于addget 类型的函数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
于 2015-07-29T16:54:29.467 回答
1

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

以下是一些可能(或可能不会!)有意义的附加技巧,具体取决于您尝试使用FooBar.

如果您在大多数情况下以相似的方式使用Foo值和值(即拥有字段和字段Bar之间的区别是一个小细节),那么将相似之处分解并使用单个构造函数是有意义的:CharInt

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

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

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

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

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

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

请注意,很多人不喜欢这Default门课,而且不是没有理由的。尽管如此,对于非常特定于您的应用程序的类型(例如配置设置)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.

这是一个温和的介绍lens重点是我在这里没有展示的方面,特别是镜头可以如何组合。

于 2015-07-29T17:15:08.927 回答