使用 Haskell 中的函数更新记录中的字段

Updating a field in a record with a function in Haskell

我想要一个可以更新给定字段的函数,我称之为“updateField”。

但是出现错误信息“error: Not in scope: `f'”。如何解决?

data Record = Record
  { a :: Int
  , b :: Int
  } deriving Show

initRecord = Record
  { a = 1
  , b = 2
  }

updateField :: Record -> (Record -> Int) -> Int -> Record
updateField rec f n = rec { f = n }

您当前正在传递类型为 Record -> Int 的函数作为 updateField 的参数。这可能是 any 函数,而不仅仅是 Record 的字段,所以即使允许传递 first-class 字段,如果调用 updateField 的函数类似于 \ _rec -> 2 + 2 而不是 ab?

在这种情况下,一个简单的替代方法是传递一个 setter 函数:

updateField
  :: Record
  -> (Record -> Int -> Record)
  -> Int
  -> Record
updateField rec setField n = setField rec n

setA, setB :: Record -> Int -> Record
setA rec n = rec { a = n }
setB rec n = rec { b = n }

用法:

updateField (updateField initRecord setA 10) setB 20

如果您还需要获取该字段,请同时传递:

modifyField
  :: Record
  -> (Record -> Int)
  -> (Record -> Int -> Record)
  -> (Int -> Int)
  -> Record
modifyField rec getField setField f
  = setField rec (f (getField rec))
modifyField
  (modifyField initRecord a setA (+ 1))
  b
  setB
  (* 2)

当然,这有点容易出错,因为你必须在调用站点同时传递 asetA,或者 bsetB &c。 ,并且它们必须匹配。对此的标准方法是将 getter 和 setter 捆绑在一起成为第一个 class 访问器,称为 lens (或更一般地 光学):

-- Required to pass a ‘Lens’ as an argument,
-- since it’s polymorphic.
{-# LANGUAGE RankNTypes #-}

import Control.Lens (Lens', set)

data Record = Record
  { a :: Int
  , b :: Int
  } deriving Show

-- ‘fieldA’ and ‘fieldB’ are first-class accessors of a
-- field of type ‘Int’ within a structure of type ‘Record’.
fieldA, fieldB :: Lens' Record Int

-- Equivalent to this function type:
--   :: (Functor f) => (Int -> f Int) -> Record -> f Record

-- Basically: extract the value, run a function on it,
-- and reconstitute the result.
fieldA f r = fmap (\ a' -> r { a = a' }) (f (a r))
fieldB f r = fmap (\ b' -> r { b = b' }) (f (b r))

initRecord :: Record
initRecord = Record
  { a = 1
  , b = 2
  }

updateField :: Record -> Lens' Record Int -> Int -> Record
updateField rec f n = set f n rec

您使用 set 设置字段,view 获取字段,over 对其应用函数。

view fieldA (updateField initRecord fieldA 10)
-- =
a (initRecord { a = 10 })
-- =
10

由于镜头是完全机械的,它们通常使用模板 Haskell 自动导出,通常通过在实际字段前加上 _ 前缀并在没有该前缀的情况下导出镜头:

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens.TH (makeLenses)

data Record = Record
  { _a :: Int
  , _b :: Int
  } deriving Show

makeLenses ''Record

-- Generated code:
--
-- a, b :: Lens' Record Int
-- a f r = fmap (\ a' -> r { _a = a' }) (f (_a r))
-- b f r = fmap (\ b' -> r { _b = b' }) (f (_b r))
view a (updateField initRecord a 10)
-- =
_a (initRecord { _a = 10 })
-- =
10

事实上,updateFieldmodifyField 现在是多余的,因为您可以使用 setover 来自 lens:

view a $ over b (* 10) $ set a 5 initRecord

Optics 在这种情况下 可能 矫枉过正,但在更大的例子中它们还有许多其他优势,因为它们允许您对复杂的嵌套数据结构进行各种访问和遍历,而无需必须手动拆开和放回记录,因此它们非常值得在某些时候添加到您的 Haskell 曲目中。 lens 包为每个可能的用例定义了许多运算符符号,但即使使用基本的命名函数,如 viewsetoverat 和等等会让你走很长的路。对于较小的依赖项,microlens 也是一个不错的选择。