具有默认字段且需要与其一起使用的函数的数据类型

时间:2015-07-29 13:46:15

标签: haskell constructor value-constructor

说,我有一个数据类型

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)

这是如何惯用的吗?只是一个通用功能?

如你所见,我是初学者,所以也许这些问题没什么意义。希望不是。

3 个答案:

答案 0 :(得分:2)

我将逐一解决您的问题。

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

  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:)是一个函数,它在List的前面添加3)并且它应用了函数&#34 ;镜头指示"。这与仿函数非常相似,但是,我们有更大的自由度(在这个例子中,仿函数实例将有义务更改列表的每个元素,而不是在列表本身上运行)。

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

答案 1 :(得分: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可让您部分应用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 :(得分:1)

(2)和(3)是完成正常和惯用的方式。特别是关于(2),您偶尔会听到的一个表达式是"智能构造函数"。这只是意味着像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非常有用。请注意,它是一个 big 库,对初学者来说可能有点压倒性,所以事先要深呼吸。这是一个小样本:

{-# 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,它侧重于我在这里没有展示过的方面,特别是可以很好地组合镜头。