【问题标题】:Haskell dynamically set record field based on field name string?Haskell根据字段名称字符串动态设置记录字段?
【发布时间】:2012-01-29 09:41:37
【问题描述】:

假设我有以下记录:

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

函数怎么写:

changeField :: Rec -> String -> Int -> Rec
changeField rec fieldName value 

这样我就可以将字符串“field1”或“field2”传入fieldName 参数并让它更新相关的字段?我知道 Data.DataData.Typeable 是在这里使用的,但我无法弄清楚这两个包。


我见过的一个库示例是 cmdArgs。下面是来自博客的excerpt,关于如何使用这个库:

{-# 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

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

$ guess --min=10
NumberGuess {min = 10, max = 100, limit = Nothing}

【问题讨论】:

  • 你可能不想这样做。你听说过lenses吗?我认为实现这一点的唯一方法是将字段名称与其参数索引配对并使用gmapQi 或类似的方法。 (您需要将 deriving (Typeable, Data) 添加到您的记录声明中才能有任何工作的希望;它不能用于任意类型。)
  • 我确实想这样做。我想创建一个库,用户可以在其中提供记录,并且该库可以通过解析一些文本来填充记录。文本将包含对我要设置的记录中的字段的引用。
  • 示例选择不当,恐怕; cmdargs 的隐式模式因其杂质而成为最受诟病的 Haskell 库之一 :) 不过,如果您确实想要完成此操作,那么我仍然建议使用 Template Haskell 生成 recMap;它更灵活,更不神奇。
  • 这种方式违背了 Haskell 编译时安全的心态。您应该考虑使用动态较少的解决方案
  • @Ana:当然可以,但是对这样一个问题的完整回答可能需要指出可能有更好的方法来实现相同的目标;如果有人问“我如何使用 unsafeCoerce 在 Haskell 中的整数类型之间进行转换?”,请不要指出您应该使用 fromIntegral 来代替;因此我的cmets。

标签: haskell record


【解决方案1】:

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

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

modifyFieldsetField 的签名在记录和修改器/值类型中都是通用的;但是,为了避免 Num 歧义,调用示例中的数字常量必须明确指定 :: Int 签名。

我还更改了参数顺序,使 rec 排在最后,从而允许通过普通函数组合创建 modifyField/setField 链(参见最后一个调用示例)。

modifyField 建立在原语 gmapTi 之上,这是 Data.Data 的“缺失”函数。它是gmapTgmapQi 之间的交叉。

{-# 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)

【讨论】:

    【解决方案2】:

    您可以构建从字段名称到镜头的映射:

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

    【讨论】:

    • recMap 正是我要避免的项目。我要求我专注于每个领域,我想动态地进行从字符串到字段的映射。
    猜你喜欢
    • 2012-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-27
    • 2019-07-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多