2015-06-26 14 views
9

Sto scrivendo un programma che convalida una complessa struttura di dati in base a una serie di regole complesse. Inserisce i dati e genera un elenco di messaggi che indicano problemi con i dati.Posso riflettere i messaggi di un programma Haskell in fase di runtime?

pensare in questa direzione:

import Control.Monad (when) 
import Control.Monad.Writer (Writer, tell) 

data Name = FullName String String | NickName String 
data Person = Person { name :: Name, age :: Maybe Int } 

data Severity = E | W | C -- error/warning/comment 
data Message = Message { severity :: Severity, code :: Int, title :: String } 
type Validator = Writer [Message] 

report :: Severity -> Int -> String -> Validator() 
report s c d = tell [Message s c d] 

checkPerson :: Person -> Validator() 
checkPerson person = do 
    case age person of 
    Nothing -> return() 
    Just years -> do 
     when (years < 0) $ report E 1001 "negative age" 
     when (years > 200) $ report W 1002 "age too large" 
    case name person of 
    FullName firstName lastName -> do 
     when (null firstName) $ report E 1003 "empty first name" 
    NickName nick -> do 
     when (null nick) $ report E 1004 "empty nickname" 

Per la documentazione, voglio anche di compilare una lista di tutti i messaggi di questo programma può produrre. Cioè, voglio ottenere il valore:

[ Message E 1001 "negative age" 
, Message W 1002 "age too large" 
, Message E 1003 "empty first name" 
, Message E 1004 "empty nickname" 
] 

potrei spostare i messaggi fuori checkPerson in qualche struttura di dati esterni, ma mi piace quando i messaggi sono definiti proprio nel punto in cui vengono utilizzati.

Potrei (e probabilmente dovrei) estrarre i messaggi dall'AST in fase di compilazione.

Ma la flessibilità propagandata di Haskell mi ha fatto riflettere: posso ottenere quello in fase di esecuzione? Cioè, posso scrivere una funzione

allMessages :: (Person -> Validator()) -> [Message] 

tale che allMessages checkPerson mi avrebbe dato la lista di cui sopra?

Naturalmente, checkPerson e Validatornon deve rimanere lo stesso.

posso quasi (non del tutto) vedere come avrei potuto fare un Validator monade con una “backdoor” che avrebbe gestito checkPerson in una sorta di “modalità di riflessione”, che attraversa tutti i percorsi e ritorno tutto Message s incontrato. Dovrei scrivere una funzione personalizzata when che saprebbe ignorare il suo primo argomento in alcune circostanze (quali?). Quindi, una specie di DSL. Forse potrei anche emulare il pattern matching?

Quindi: posso fare qualcosa di simile, come, e cosa dovrei sacrificare?

Non esitate a suggerire soluzioni, anche se non si adattano esattamente alla descrizione precedente.

+0

Questo è un problema piuttosto difficile in generale, in sostanza si sta cercando di scrivere uno strumento di analisi statico per il proprio DSL. Potresti scrivere un DSL di questo tipo in Haskell usando le monade libere abbastanza facilmente, in realtà, ma eseguendo l'analisi per estrarre tutti i messaggi possibili, tuttavia sarà difficile dal momento che il valore di un messaggio può essere determinato esclusivamente in fase di runtime. Se limiti i tuoi 'title's e' code's usando semplici tipi di dati di somma, allora sarebbe un po 'più semplice, ma hai ancora il problema che alcuni valori potrebbero essere determinati solo dai valori di runtime. – bheklilr

+0

@bheklilr Spero che la mia risposta faccia esplodere la tua mente. =) –

+0

@DanielWagner fa un po ', sì! Non avrei pensato a quell'approccio. – bheklilr

risposta

10

Questo tipo di analisi semi-statica è fondamentalmente esattamente ciò per cui sono state inventate le frecce. Quindi facciamo una freccia! La nostra freccia sarà fondamentalmente solo un'azione Writer, ma quella che ricorda quali messaggi potrebbe sputare in un dato momento. In primo luogo, un po 'boilerplate:

{-# LANGUAGE Arrows #-} 

import Control.Arrow 
import Control.Category 
import Control.Monad.Writer 
import Prelude hiding (id, (.)) 

Ora, del tipo sopra descritto:

data Validator m a b = Validator 
    { possibleMessages :: [m] 
    , action :: Kleisli (Writer m) a b 
    } 

runValidator :: Validator m a b -> a -> Writer m b 
runValidator = runKleisli . action 

Ci sono alcuni casi semplici da mettere in atto. Di particolare interesse: la composizione di due validatori ricorda i messaggi sia della prima azione che della seconda azione.

instance Monoid m => Category (Validator m) where 
    id = Validator [] id 
    Validator ms act . Validator ms' act' = Validator (ms ++ ms') (act . act') 

instance Monoid m => Arrow (Validator m) where 
    arr f = Validator [] (arr f) 
    first (Validator ms act) = Validator ms (first act) 

instance Monoid m => ArrowChoice (Validator m) where 
    left (Validator ms act) = Validator ms (left act) 

Tutta la magia è l'operazione che permette in realtà di segnalare qualcosa:

reportWhen :: Monoid m => m -> (a -> Bool) -> Validator m a() 
reportWhen m f = Validator [m] (Kleisli $ \a -> when (f a) (tell m)) 

Questa è l'operazione che si accorge quando si è in procinto di emettere un possibile messaggio, e fa una nota di esso.Copia i tuoi tipi e mostra come codificare checkPerson come una freccia. Ho semplificato un po 'i tuoi messaggi, ma in questo caso non c'è nulla di importante, solo un po' meno sintattico di overhead nell'esempio.

type Message = String 
data Name = FullName String String | NickName String -- http://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/ 
data Person = Person { name :: Name, age :: Maybe Int } 

checkPerson :: Validator Message Person() 
checkPerson = proc person -> do 
    case age person of 
     Nothing -> returnA -<() 
     Just years -> do 
      "negative age" `reportWhen` (< 0) -< years 
      "age too large" `reportWhen` (>200) -< years 
    case name person of 
     FullName firstName lastName -> do 
      "empty first name" `reportWhen` null -< firstName 
     NickName nick -> do 
      "empty nickname" `reportWhen` null -< nick 

spero che sarete d'accordo che questa sintassi non è troppo lontana da quello che originariamente scritto. Vediamolo in azione in ghci:

> runWriter (runValidator checkPerson (Person (NickName "") Nothing)) 
((),"empty nickname") 
> possibleMessages checkPerson 
["empty nickname","empty first name","age too large","negative age"] 
+4

Inoltre, adoro il modo discreto con cui hai nascosto quel collegamento. –

Problemi correlati