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

Haskell根据字段名字符串动态设置记录字段?,haskell,record,Haskell,Record,假设我有以下记录: data Rec = Rec { field1 :: Int, field2 :: Int } 如何编写函数: changeField :: Rec -> String -> Int -> Rec changeField rec fieldName value 这样我就可以将字符串“field1”或“field2”传递到fieldName参数中,并让它更新相关字段?我理解数据。这里使用的是Data和数据。Typeable,但我无法理解这两个

假设我有以下记录:

data Rec = Rec {
   field1 :: Int,
   field2 :: Int
}
如何编写函数:

changeField :: Rec -> String -> Int -> Rec
changeField rec fieldName value 
这样我就可以将字符串“field1”或“field2”传递到
fieldName
参数中,并让它更新相关字段?我理解
数据。这里使用的是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

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

{-# 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''
或不带镜片:

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''

好的,这里有一个解决方案,它不使用模板haskell,也不需要您手动管理字段映射

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

modifyField
setField
的签名在记录和mutator/value类型中都是通用的;但是,为了避免
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)

你可能不想这么做。你听说了吗?我认为实现这一点的唯一方法是将字段名与其参数索引配对,并使用
gmapQi
或类似工具。(您需要将
派生(可键入,数据)
添加到您的记录声明中,这样才有希望工作;它不能用于任意类型。)我确实想这样做。我想创建一个库,用户可以在其中提供记录,库可以通过解析一些文本来填充记录。文本将包含对我要设置的记录中字段的引用;cmdargs的隐式模式是最受诟病的Haskell库之一,因为它的杂质:)不过,如果您确实想实现这一点,那么我仍然建议使用模板Haskell生成
recMap
;它更灵活,也不那么神奇。这种做法违背了Haskell的编译时安全思想。你应该考虑使用更少的动态。solution@Ana:当然可以,但对这样一个问题的完整回答可能需要指出,可能有更好的方法来实现同样的目标;如果有人问“我如何使用
unsafeccerce
在Haskell中的整数类型之间进行转换?”,如果不指出您应该使用
fromIntegral
,那将是失职的;因此,我的评论。recMap正是我正在避免的项目。我需要专门处理每个字段,并且我希望动态地完成从字符串到字段的映射。
{-# 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)