2011-12-28 13 views
10

Decir que tengo el siguiente registro:Haskell establece dinámicamente el campo de registro en función de la cadena de nombre de campo?

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

¿Cómo se escribe la función:

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

tal que me puede pasar en las cadenas "campo1" o "campo2" en el argumento fieldName y ¿ha actualizado el campo asociado? Entiendo que Data.Data y Data.Typeable son los que debo usar aquí, pero no puedo entender estos dos paquetes.


Un ejemplo de una biblioteca que he visto hacer esto es cmdArgs. A continuación se muestra un excerpt de una entrada de blog sobre el uso de esta biblioteca:

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

Ahora tenemos un analizador simple línea de comandos. Algunas reacciones de la muestra son:

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

Probablemente no desee hacer esto. ¿Has oído hablar de [lentes] (http://stackoverflow.com/questions/5767129/lenses-fclabels-data-accessor-which-library-for-structure-access-and-mutatio)? Creo que la única manera de lograr esto sería un truco que involucre emparejar nombres de campo con sus índices de argumento y usar 'gmapQi' o similar. (Necesitaría agregar 'derivación (Typeable, Data)' a su declaración de registro para que esto tenga alguna esperanza de funcionar, no se puede hacer para tipos arbitrarios). – ehird

+1

Sí quiero hacer esto. Me gustaría crear una biblioteca donde el usuario pueda proporcionar un registro, y la biblioteca puede llenar el registro analizando algún texto. El texto contendrá referencias al campo en el registro que quiero establecer. – Ana

+0

Es mejor evitar vincular la implementación de esta funcionalidad orientada al usuario a los detalles de la implementación interna de los nombres de los campos de registro. En segundo lugar sugiero la solución basada en lentes; podría automatizar la creación de 'recMap' a partir de nombres de campo de registro con Template Haskell. – ehird

Respuesta

6

bien, he aquí una solución que doesn No use haskell de plantilla, o requiera que administre el mapa de campo manualmente.

I implementó una más general modifyField que acepta una función mutador, y aplicarse setField (nee changeField) usarlo con const value.

La firma de modifyField y setField es genérica tanto en el registro como en el tipo de mutador/valor; sin embargo, para evitar la ambigüedad de Num, las constantes numéricas en el ejemplo de invocación deben tener explícitamente las firmas :: Int.

También ha cambiado el orden de los parámetros de modo rec viene al final, lo que permite una cadena de modifyField/setField a ser creado por la composición de la función normal (ver el último ejemplo invocación).

modifyField se basa en la primitiva gmapTi, que es una función 'faltante' de Data.Data. Es un cruce entre gmapT y 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) 
4

se puede construir un mapa de los nombres de campo a sus lentes:

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

o sin lentes:

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

El recMap es precisamente el elemento que estoy evitando. Necesito que me especialice para cada campo, y me gustaría hacer el mapeo de una cadena a otra de manera dinámica. – Ana

Cuestiones relacionadas