您当前正在传递一个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)
当然,这有点容易出错,因为您必须在调用站点同时传递a 和setA,或者b 和setB 等,并且它们必须匹配。解决这个问题的标准方法是将 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
事实上,updateField 和 modifyField 现在是多余的,因为您可以只使用来自lens 的set 和over:
view a $ over b (* 10) $ set a 5 initRecord
光学在这种情况下可能有点矫枉过正,但在更大的示例中它们还有许多其他优点,因为它们让您无需手动拆开复杂的嵌套数据结构即可进行各种访问和遍历并将记录重新组合在一起,因此它们非常值得在某个时候添加到您的 Haskell 曲目中。 lens 包为每个可能的用例定义了许多运算符符号,但即使使用基本的命名函数,如 view、set、over、at 等,也会让你走得很远。对于较小的依赖项,还有microlens 作为一个不错的选择。