ОК, вот решение, которое не использует шаблон haskell или требует, чтобы вы управляли картой поля вручную.
Я реализовал более общий modifyField
, который принимает функцию мутатора, и реализовал setField
(урожд. changeField
), используя ее с const value
.
Сигнатура modifyField
и setField
является общей как для типа записи, так и для типа мутатора / значения; однако во избежание неоднозначности Num
числовые константы в примере вызова должны иметь явные :: Int
подписи.
Я также изменил порядок параметров, чтобы rec
был последним, что позволило создать цепочку modifyField
/ setField
с помощью обычной композиции функций (см. Последний пример вызова).
modifyField
построен поверх примитива gmapTi
, который является «отсутствующей» функцией из Data.Data
. Это нечто среднее между gmapT
и gmapQi
.
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE RankNTypes #-}
import Data.Typeable (Typeable, typeOf)
import Data.Data (Data, gfoldl, gmapQi, ConstrRep(AlgConstr),
toConstr, constrRep, constrFields)
import Data.Generics (extT, extQ)
import Data.List (elemIndex)
import Control.Arrow ((&&&))
data Rec = Rec {
field1 :: Int,
field2 :: String
} deriving(Show, Data, Typeable)
main = do
let r = Rec { field1 = 1, field2 = "hello" }
print r
let r' = setField "field1" (10 :: Int) r
print r'
let r'' = setField "field2" "world" r'
print r''
print . modifyField "field1" (succ :: Int -> Int) . setField "field2" "there" $ r
print (getField "field2" r' :: String)
---------------------------------------------------------------------------------------
data Ti a = Ti Int a
gmapTi :: Data a => Int -> (forall b. Data b => b -> b) -> a -> a
gmapTi i f x = case gfoldl k z x of { Ti _ a -> a }
where
k :: Data d => Ti (d->b) -> d -> Ti b
k (Ti i' c) a = Ti (i'+1) (if i==i' then c (f a) else c a)
z :: g -> Ti g
z = Ti 0
---------------------------------------------------------------------------------------
fieldNames :: (Data r) => r -> [String]
fieldNames rec =
case (constrRep &&& constrFields) $ toConstr rec of
(AlgConstr _, fs) | not $ null fs -> fs
otherwise -> error "Not a record type"
fieldIndex :: (Data r) => String -> r -> Int
fieldIndex fieldName rec =
case fieldName `elemIndex` fieldNames rec of
Just i -> i
Nothing -> error $ "No such field: " ++ fieldName
modifyField :: (Data r, Typeable v) => String -> (v -> v) -> r -> r
modifyField fieldName m rec = gmapTi i (e `extT` m) rec
where
i = fieldName `fieldIndex` rec
e x = error $ "Type mismatch: " ++ fieldName ++
" :: " ++ (show . typeOf $ x) ++
", not " ++ (show . typeOf $ m undefined)
setField :: (Data r, Typeable v) => String -> v -> r -> r
setField fieldName value = modifyField fieldName (const value)
getField :: (Data r, Typeable v) => String -> r -> v
getField fieldName rec = gmapQi i (e `extQ` id) rec
where
i = fieldName `fieldIndex` rec
e x = error $ "Type mismatch: " ++ fieldName ++
" :: " ++ (show . typeOf $ x) ++
", not " ++ (show . typeOf $ e undefined)