Haskell根据字段名字符串动态设置记录字段?

Ana*_*Ana 10 haskell record

说我有以下记录:

data Rec = Rec {
   field1 :: Int,
   field2 :: Int
}
Run Code Online (Sandbox Code Playgroud)

我该如何编写这个函数:

changeField :: Rec -> String -> Int -> Rec
changeField rec fieldName value 
Run Code Online (Sandbox Code Playgroud)

这样我可以将字符串"field1"或"field2"传入fieldName参数并让它更新相关字段?我理解Data.Data并且Data.Typeable在这里使用的是什么,但我无法想出这两个包.


我见过的库的一个例子就是cmdArgs.以下是关于如何使用此库的博客文章的摘录:

{-# LANGUAGE DeriveDataTypeable #-}
import System.Console.CmdArgs

data Guess = Guess {min :: Int, max :: Int, limit :: Maybe Int} deriving (Data,Typeable,Show)

main = do
    x <- cmdArgs $ Guess 1 100 Nothing
    print x
Run Code Online (Sandbox Code Playgroud)

现在我们有一个简单的命令行解析器.一些示例交互是:

$ guess --min=10
NumberGuess {min = 10, max = 100, limit = Nothing}
Run Code Online (Sandbox Code Playgroud)

pat*_*pat 8

好的,这是一个不使用模板haskell的解决方案,或者需要您手动管理字段映射.

我实现了一个更通用的modifyField接受mutator函数,并使用它实现setField(nee changeField)const value.

记录和mutator/value类型的签名modifyFieldsetField通用; 但是,为了避免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)
Run Code Online (Sandbox Code Playgroud)


pat*_*pat 6

您可以从字段名称到其镜头构建地图:

{-# 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''
Run Code Online (Sandbox Code Playgroud)

或没有镜头:

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''
Run Code Online (Sandbox Code Playgroud)

  • recMap 正是我要避免的项目。我要求我专门研究每个领域,并且我想动态地进行从字符串到字段的映射。 (2认同)