haskell - 使用 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 }
解决方案
您当前正在传递一个类型的函数Record -> Int
作为 的参数updateField
。这可以是任何函数,而不仅仅是 的字段Record
,所以即使允许传递一流的字段,如果您updateField
使用类似\ _rec -> 2 + 2
而不是a
or的函数调用会发生什么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 )的一流访问器中:setA
b
setB
-- 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
是多余的,因为您可以使用set
and over
from lens
:
view a $ over b (* 10) $ set a 5 initRecord
在这种情况下,光学可能有点矫枉过正,但在更大的示例中它们还有许多其他优势,因为它们允许您对复杂的嵌套数据结构进行各种访问和遍历,而无需手动拆开记录并将其重新组合在一起,因此它们是非常值得在某个时候添加到您的 Haskell 曲目中。该lens
包为每个可能的用例定义了许多运算符符号,但即使使用基本的命名函数,如view
、set
、over
、at
等,也会让你走得很远。对于较小的依赖项,还有microlens
一个很好的选择。
推荐阅读
- mysql - MySQL 基于客户的不同字段计数
- javascript - 如何在 Input 中设置值并选择
- c++ - 替代获取标准库函数的地址/可能是格式错误的行为
- javascript - 创建一个数组,其中包含对特定数字求和所需的数字
- python - 使用 PIL (python) 将图像数据集加载到内存中会消耗太多内存
- php - 使用 PHP 检查 SSL 证书哈希
- c - scanf("%d/%d/%d) 在 C 中是什么意思?
- flutter - 如何在 Flutter 中访问 GridView 中特定项目的索引?
- javascript - 使用 react redux 探针获取异步数据
- php - 如何解决贪婪的正则表达式