首页 > 解决方案 > 使用 Haskell 中的函数更新记录中的字段

问题描述

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

但是有一条错误消息“错误:不在范围内:`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 }

标签: haskellsyntaxrecordassign

解决方案


您当前正在传递一个类型的函数Record -> Int作为 的参数updateField。这可以是任何函数,而不仅仅是 的字段Record,所以即使允许传递一流的字段,如果您updateField使用类似\ _rec -> 2 + 2而不是aor的函数调用会发生什么b

在这种情况下,一个简单的替代方法是传递一个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)

当然,这有点容易出错,因为您必须在调用站点同时传递a和,或者和&c.,并且它们必须匹配。解决这个问题的标准方法是将 getter 和 setter 捆绑到一个称为lens(或更一般地optic )的一流访问器中:setAbsetB

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

由于镜头是完全机械的,它们通常使用 Template 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

事实上,updateField现在modifyField是多余的,因为您可以使用setand overfrom lens

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

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


推荐阅读