Haskell dynamically set record field based on field name string?

后端 未结 2 1953
你的背包
你的背包 2020-12-09 12:27

Say I have the following record:

data Rec = Rec {
   field1 :: Int,
   field2 :: Int
}

How do I write the function:

changeF         


        
相关标签:
2条回答
  • 2020-12-09 12:49

    You can build a map from the field names to their lenses:

    {-# LANGUAGE TemplateHaskell #-}
    import Data.Lens
    import Data.Lens.Template
    import qualified Data.Map as Map
    
    data Rec = Rec {
        _field1 :: Int,
        _field2 :: Int
    } deriving(Show)
    
    $( makeLens ''Rec )
    
    recMap = Map.fromList [ ("field1", field1)
                          , ("field2", field2)
                          ]
    
    changeField :: Rec -> String -> Int -> Rec
    changeField rec fieldName value = set rec
        where set = (recMap Map.! fieldName) ^= value
    
    main = do
      let r = Rec { _field1 = 1, _field2 = 2 }
      print r
      let r' = changeField r "field1" 10
      let r'' = changeField r' "field2" 20
      print r''
    

    or without lenses:

    import qualified Data.Map as Map
    
    data Rec = Rec {
        field1 :: Int,
        field2 :: Int
    } deriving(Show)
    
    recMap = Map.fromList [ ("field1", \r v -> r { field1 = v })
                          , ("field2", \r v -> r { field2 = v })
                          ]
    
    changeField :: Rec -> String -> Int -> Rec
    changeField rec fieldName value =
        (recMap Map.! fieldName) rec value
    
    main = do
      let r = Rec { field1 = 1, field2 = 2 }
      print r
      let r' = changeField r "field1" 10
      let r'' = changeField r' "field2" 20
      print r''
    
    0 讨论(0)
  • 2020-12-09 13:06

    OK, here's a solution that doesn't use template haskell, or require you to manage the field map manually.

    I implemented a more general modifyField which accepts a mutator function, and implemented setField (nee changeField) using it with const value.

    The signature of modifyField and setField is generic in both the record and mutator/value type; however, in order to avoid Num ambiguity, the numeric constants in the invocation example have to be given explicit :: Int signatures.

    I also changed the parameter order so rec comes last, allowing a chain of modifyField/setField to be created by normal function composition (see the last invocation example).

    modifyField is built on top of the primitive gmapTi, which is a 'missing' function from Data.Data. It is a cross between gmapT and 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)
    
    0 讨论(0)
提交回复
热议问题