【问题标题】:Updating a field in a record with a function in Haskell使用 Haskell 中的函数更新记录中的字段
【发布时间】:2020-11-25 17:40:50
【问题描述】:

我想要一个可以更新给定字段的函数,我称之为“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 }

【问题讨论】:

  • 为什么要在函数中使用Int 作为参数?
  • 您不能参数化该字段,或者至少不能这样。 lenses 通常用于此目的。见hackage.haskell.org/package/lens
  • 会调查的,谢谢。
  • 我希望函数采用 Int,因为这将在给定字段中被替换。

标签: haskell syntax record assign


【解决方案1】:

您当前正在传递一个Record -> Int 类型的函数作为updateField 的参数。这可能是任何函数,而不仅仅是Record的字段,所以即使允许传递一等字段,如果你用\ _rec -> 2 + 2之类的函数调用updateField会发生什么是a 还是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)

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-07-03
    • 2016-10-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多