2015-06-26 10 views
9

Ich schreibe ein Programm, das eine komplexe Datenstruktur nach einer Reihe komplexer Regeln validiert. Er gibt die Daten ein und gibt eine Liste von Nachrichten aus, die Probleme mit den Daten anzeigen.Kann ich zur Laufzeit Meldungen aus einem Haskell-Programm reflektieren?

Denken in diese Richtung:

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" 

Zur Dokumentation, möchte ich auch dieses Programm ausgeben kann eine Liste aller Nachrichten kompilieren. Das heißt, ich mag den Wert erhalten:

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

ich die Nachrichten aus checkPerson in eine externe Datenstruktur bewegen konnte, aber ich mag es, wenn die Nachrichten direkt an Ort und Stelle festgelegt werden, in denen sie verwendet werden.

Ich könnte (und wahrscheinlich sollte) die Nachrichten aus dem AST zur Kompilierzeit extrahieren.

Aber die angepriesene Flexibilität von Haskell ließ mich denken: kann ich das erreichen zur Laufzeit? Das heißt, kann ich eine Funktion

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

so schreiben, dass allMessages checkPerson würde ich die oben aufgeführte Liste?

Natürlich checkPerson und Validatormüssen nicht bleiben gleich.

Ich kann fast (nicht ganz) sehen, wie ich eine benutzerdefinierte Validator Monade mit einer „Hintertür“ machen könnte, die checkPerson in einer Art laufen würde „Reflexionsmodus“ alle Wege durchlaufen und die Rückkehr alle Message begegnet s. Ich müsste eine benutzerdefinierte when-Funktion schreiben, die unter bestimmten Umständen wissen könnte (welches?), Ihr erstes Argument zu ignorieren. Also, eine Art DSL. Vielleicht könnte ich sogar Mustervergleiche emulieren?

Also: kann ich so etwas tun, wie und was hätte ich zu opfern?

Bitte zögern Sie nicht Lösungen vorschlagen, auch wenn sie nicht genau der obigen Beschreibung entsprechen.

+0

Dies ist ein ziemlich schwieriges Problem im Allgemeinen ist, im Wesentlichen wollen, sind Sie ein statisches Analyse-Tool für Ihren DSL zu schreiben. Sie könnten eine solche DSL in Haskell schreiben, indem Sie einfach kostenlose Monaden verwenden, aber die Analyse wird durchgeführt, um alle möglichen Nachrichten herauszuziehen, aber das wird schwierig, da der Wert einer Nachricht nur zur Laufzeit bestimmt werden kann. Wenn Sie Ihre Titel und Codes mit einfachen Summendatentypen einschränken, wäre es etwas einfacher, aber Sie haben immer noch das Problem, dass einige Werte nur durch Laufzeitwerte bestimmt werden können. – bheklilr

+0

@bheklilr Ich hoffe, dass meine Antwort Ihre Gedanken bläst. =) –

+0

@DanielWagner es tut ein bisschen, yeah! An diesen Ansatz hätte ich überhaupt nicht gedacht. – bheklilr

Antwort

10

Diese halbstatische Analyse ist genau das, wofür die Pfeile erfunden wurden. Also machen wir einen Pfeil! Unser Pfeil wird im Grunde nur eine Writer Aktion sein, aber eine, die daran erinnert, welche Nachrichten sie zu irgendeinem gegebenen Zeitpunkt ausgespuckt haben könnte. Zuerst einige Textvorschlag:

{-# LANGUAGE Arrows #-} 

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

nun beschriebene Art oben:

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 

Es gibt einige einfache Fälle, in Stelle zu setzen. Von besonderem Interesse: Die Zusammensetzung von zwei Validatoren erinnert sich an Nachrichten von der ersten Aktion und der zweiten Aktion.

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) 

die alle Magie ist in der Operation, die Sie berichten, tatsächlich etwas lässt:

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

Dies ist der Vorgang, dass, wenn Sie eine mögliche Meldung bemerkt, sind über die Ausgabe und macht eine Notiz von es.Lassen Sie uns Ihre Typen kopieren und zeigen Sie, wie Sie checkPerson als Pfeil codieren. Ich habe Ihre Nachrichten ein wenig vereinfacht, aber nichts Wichtiges ist anders - nur weniger syntaktischer Overhead im Beispiel.

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 

Ich hoffe, Sie werden mir zustimmen, dass diese Syntax ist nicht zu weit entfernt von dem, was Sie ursprünglich geschrieben. Lassen Sie sich in Aktion in GHCI sehen:

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

Außerdem liebe ich, wie unauffällig Sie den Link dorthin geschlichen haben. –

Verwandte Themen