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

时间:2017-12-09 13:17:19

标签: haskell record

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

2 个答案:

答案 0 :(得分:7)

概括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效果)读取计数器增加。

我们可以给函数类型synonym,给定一种(可能有效的)更改字段的方法,返回一个转换整个记录的函数:

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}

我们已经完成了修改功能及其可能的效果,但我们仍然需要定义这些&#34;镜头&#34;对于每个领域,这很烦人。在实践中,人们使用Template Haskell自动定义它们并避免使用样板。

但是如果我们想要一个让我们指定字段名称的单个函数呢?可悲的是,这更复杂。您需要一种方法将类型级别的字符串作为参数传递,并使用multi-parameter type class来编码字段名称,记录类型和字段类型之间的关系。有some packages这样做(同样,模板Haskell帮助为样板)但据我所知,它们没有被广泛使用。

镜头的主库称为lens,还有microlens,这是一种具有较轻依赖性足迹的替代方案。它们是可互操作的:使用一个库定义的镜头与另一个库一起工作。

答案 1 :(得分: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