2011-12-28 21 views
10

Dire che ho il seguente record:Haskell ha impostato dinamicamente il campo del record in base alla stringa del nome del campo?

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

Come faccio a scrivere la funzione:

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

tale che posso passare in stringhe "campo1" o "field2" in l'argomento fieldName e aggiornare il campo associato? Capisco Data.Data e Data.Typeable sono cosa usare qui, ma non riesco a capire questi due pacchetti.


Un esempio di libreria che ho visto fare questo è cmdArgs. Qui di seguito è un excerpt da un post sul blog su come usare questa libreria:

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

Ora abbiamo un parser semplice riga di comando. Alcune interazioni di esempio sono:

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

Probabilmente non vuoi farlo. Hai sentito parlare di [obiettivi] (http://stackoverflow.com/questions/5767129/lenses-fclabels-data-accessor-which-library-for-structure-access-and-mutatio)? Penso che l'unico modo per ottenere questo risultato sarebbe un trucco che coinvolga l'associazione di nomi di campi con i loro indici di argomenti e l'uso di 'gmapQi' o simili. (Dovresti aggiungere 'derivando (Typeable, Data)' alla tua dichiarazione di registrazione per questo per avere qualche speranza di lavorare, non può essere fatto per tipi arbitrari.) – ehird

+1

Voglio farlo. Mi piacerebbe creare una libreria in cui l'utente può fornire un record e la libreria può popolare il record analizzando del testo. Il testo conterrà i riferimenti al campo nel record che voglio impostare. – Ana

+0

È meglio evitare di legare l'implementazione di questa funzionalità rivolta all'utente ai dettagli di implementazione interni dei nomi dei campi dei record. Ho suggerito la soluzione basata su obiettivi @pat; puoi automatizzare la creazione di 'recMap' dai nomi dei campi dei record con Template Haskell. – ehird

risposta

6

OK, ecco una soluzione che doesn utilizzare il modello haskell o richiedere la gestione manuale della mappa del campo.

ho implementato un più generale modifyField che accetta una funzione mutatore, e attuato setField (nee changeField) utilizzando con const value.

La firma di modifyField e setField è generica sia nel tipo di record che di mutatore/valore; tuttavia, al fine di evitare l'ambiguità Num, le costanti numeriche nell'esempio di chiamata devono essere fornite con le firme esplicite :: Int.

Ho anche cambiato l'ordine dei parametri così rec arriva ultimo, permettendo una catena di modifyField/setField che vengano create dal composizione normale funzione (vedere l'ultima chiamata esempio).

modifyField è costruito sopra il primitivo gmapTi, che è una funzione 'mancante' da Data.Data. È un incrocio tra gmapT e 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

si può costruire una mappa dei nomi dei campi per le loro lenti:

{-# 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 senza lenti:

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

recMap è esattamente l'elemento che sto evitando. Mi richiede di specializzarmi per ogni campo e mi piacerebbe eseguire la mappatura da stringa a campo in modo dinamico. – Ana

Problemi correlati