如何简明地修改记录的字段?

6
data Person = Person
  {
    name :: String
  , counter :: Int
  }

incrementPersonCounter :: Person -> Person
incrementPersonCounter p@(Person _ c) = p { counter = c + 1 }

有没有更简洁的方法来完成上述操作?是否有一种可以使用的函数,我可以指定记录、其中一个字段(name / counter)和要应用于返回值的函数?


我想的是这样:

applyRecord r f f' = r
  { f = f' (f r) }

尽管这样做不起作用,因为:

error: Not in scope: ‘f’
   |
13 |   { f = f' (f r) }

7
这就是镜片被发明出来解决的那种问题。 - melpomene
参见有没有Haskell习惯用语来更新嵌套数据结构?,尽管我犹豫是否将其标记为重复,因为现代答案已经发生了很大变化。这个问题更好地阐述了现代答案。 - Daniel Wagner
2个回答

8

泛化 incrementPersonCounter 的一种方法是通过抽象修改函数:

modifyPersonCounter :: (Int -> Int) -> Person -> Person
modifyPersonCounter f p = (\c -> p { counter = c}) $ f (counter p)

实际上,一个常见的模式是在领域中对我们想要执行的效果进行抽象。
counterLens :: forall f. Functor f => (Int -> f Int) -> (Person -> f Person)
counterLens f p = (\c -> p { counter = c }) <$> f (counter p)

例如,我们可能想要从控制台或数据库(两者都是IO效应)中读取计数器增量。
我们可以为函数类型提供一个同义词,它接受一种(可能是有副作用的)改变字段方式,并返回一个转换整个记录的函数。这可以通过镜头实现。
type Lens' a b = forall f. Functor f => (b -> f b) -> (a -> f a)

要纯粹地修改记录,现在我们需要调用一个辅助函数 over,只需定义一次:

over :: Lens' a b -> (b -> b) -> a -> a
over l f p = runIdentity $ l (Identity . f) p

例如:

*Main> over counterLens (+1) (Person "foo" 40)
Person {name = "foo", counter = 41}

我们已经对修改函数及其可能产生的效果进行了抽象处理,但我们仍需要为每个字段定义这些“镜头”,这很烦人。实际上,人们使用 Template Haskell 自动定义它们并避免样板代码。
但是,如果我们想要一个 单一 函数来指定字段的名称呢?那就更加复杂了。您需要一种方式将类型级字符串作为参数传递,并且还需要一个多参数类型类,用于编码字段名称、记录类型和字段类型之间的关系。有一些包可以做到这一点(同样需要 Template Haskell 帮助消除样板代码),但据我所知它们并不常用。
镜头的主要库称为lens,还有microlens,是一个依赖占用更小的替代库。它们是可互操作的:使用一个库定义的镜头可以在另一个库中使用。

“generic-lens”库提供了一个名为HasField的类型,它可以为所有派生自Generic的记录提供通用的field镜头。不需要使用Template Haskell即可运行。http://hackage.haskell.org/package/generic-lens-0.5.0.0/docs/Data-Generics-Product-Fields.html#t:HasField 更多详情请见:http://kcsongor.github.io/generic-lens/ - danidiaz

5

使用lens,您可以这样编写:

incrementPersonCounter :: Person -> Person
incrementPersonCounter = counter +~ 1

例子:

λ> incrementPersonCounter $ Person "foo" 42
Person {_name = "foo", _counter = 43}

完整代码:
{-# LANGUAGE TemplateHaskell #-}

module Lib where

import Control.Lens

data Person = Person
  {
    _name :: String
  , _counter :: Int
  } deriving (Show, Eq)
makeLenses ''Person

incrementPersonCounter :: Person -> Person
incrementPersonCounter = counter +~ 1

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