2012-11-02 9 views
7

TL; DR: Ich brauche Hilfe herausfinden, wie man Code erzeugt, der eine von einer kleinen Anzahl von Datentypen (wahrscheinlich nur Double und Bool) zurückgibt aus verschiedenen Bereichen auf unterschiedlichen Datensätzen.Wie baue ich ein DSL zum Nachschlagen von Feldern aus einem Datensatz in Haskell

Langform: Angenommen, die folgenden Datentypen

data Circle = Circle { radius :: Integer, origin :: Point } 
data Square = Square { side :: Integer } 

und einige Standardcode

circle = Circle 3 (Point 0 0) 
square = Square 5 

Ich bin ein kleiner DSL bauen, und wollen, dass der Benutzer etwas wie das schreiben folgende

circle.origin 
square.side 

und es wird g enerate Code ähnlich

origin . circle 
side . square 

dies beim Parsen, würde ich die Saiten „Kreis“ hat und „Herkunft“ zum Beispiel. Ich muss diese jetzt zu Funktionsaufrufen machen. Ich könnte natürlich so etwas haben:

data Expr a = IntegerE (a -> Integer) 
      | PointE (a -> Point) 

lookupF2I "side" = Just $ IntegerE side 
lookupF2I "radius" = Just $ IntegerE radius 
lookupF2I _  = Nothing 

lookupF2P "origin" = Just $ PointE origin 
lookupF2P _ = Nothing 

und haben eine Lookup-Funktion pro zurückgegebenen Datentyp. Eine Funktion pro Datentyp ist aus Sicht von DSL praktisch, da nur zwei oder drei Datentypen behandelt werden. Dies scheint jedoch nicht besonders effektiv zu sein. Gibt es einen besseren Weg (sicher), dies zu tun? Wenn nicht, gibt es eine Möglichkeit, den Code für die verschiedenen Suchfunktionen aus den verschiedenen Datensätzen zu generieren, aus denen ich Felder suchen kann?

Zweitens gibt es immer noch die Frage des analysierten "circle" oder "square" benötigen Sie die entsprechende circle oder square Funktion aufzurufen. Wenn ich dies mit Typ-Klassen implementieren, könnte ich so etwas wie:

instance Lookup Circle where 
    lookupF2I "radius" = Just $ IntegerE radius 
    lookupF2I _  = Nothing 
    lookupF2P "origin" = Just $ PointE origin 
    lookupF2P _  = Nothing 

aber dann, das mich verläßt mit welcher Art, um herauszufinden, auf der Lookup-Funktion zu erzwingen, und schlechter zu müssen Hand schreiben Instanzen für jeden (von vielen) Datensätzen, für die ich diesen verwenden möchte.

Hinweis: Die Tatsache, dass Circle und Square mit einem einzigen ADT dargestellt werden könnte, ist nebenbei meine Frage, dass dies ein künstliches Beispiel ist. Der eigentliche Code wird verschiedene sehr unterschiedliche Datensätze enthalten, von denen die einzigen, die sie gemeinsam haben, Felder desselben Typs sind.

+6

Ich würde auf das Lens-Paket schauen: http://hackage.haskell.org/package/lens-3.1 und auch bei Vinyl https://github.com/jonsterling/Vinyl – ErikR

+1

+1 für "Linse". Es unterstützt Template Haskell, sodass Sie Linsen automatisch aus Datentypen erstellen können. Dann übersetzt Ihr Parsing-Code die Strings einfach in ihre entsprechenden Objektive. –

+2

Es sieht so aus, als ob Sie Ihr DSL mit einer Art HOAS (abstrakte Syntax höherer Ordnung) einbetten, die Funktionen aus der Metasprache (Haskell) in der eingebetteten Sprache verwendet. Wenn Sie ein paar Anmerkungen zur Verwendung von HOAS haben möchten, dann schauen Sie sich das "Unembedding" -Papier von Robert Atkey und Co-Autoren https://personal.cis.strath.ac.uk/robert.atkey/unembedding.html an. Wenn HOAS Ihnen nichts bedeutet, dann sind Sie wahrscheinlich mit der normalen abstrakten Syntax erster Ordnung am besten - stellen Sie eingebaute Funktionen mit einem Bezeichner dar und bewerten Sie sie mit einer Umgebung, die eine Suche nach den primitiven Funktionen enthält. –

Antwort

1

Ich habe versucht, Vorlage Haskell zu verwenden, um eine nette und typsichere Möglichkeit zur Lösung dieses Problems bereitzustellen. Um dies zu tun, konstruierte ich die Ausdrücke aus einer gegebenen Zeichenfolge.

Ich nehme an, dass Lens-Paket kann das tun, aber es kann eine einfachere und flexiblere Lösung sein.

Es kann wie folgt verwendet werden:

import THRecSyntax 
circleOrigin = compDSL "circle.origin.x" 

und ist definiert wie folgt aus:

{-# LANGUAGE TemplateHaskell #-} 
import Language.Haskell.TH 

compDSL :: String -> Q Exp 
compDSL s = return 
      $ foldr1 AppE 
      $ map (VarE . mkName) 
      (reverse $ splitEvery '.' s) 

das Ergebnis Ausdruck wird also: x (origin circle)

Hinweis: splitEvery ist eine Funktion, teilt eine Liste in Unterlisten auf, die das gegebene Element herausnehmen. Beispielimplementierung:

splitEvery :: Eq a => a -> [a] -> [[a]] 
splitEvery elem s = splitter (s,[]) 
    where splitter (rest, res) = case elemIndex elem rest of 
      Just dotInd -> let (fst,rest') = splitAt dotInd rest 
          in splitter (tail rest', fst : res) 
      Nothing -> reverse (rest : res) 

Dies ist ein Schwergewicht aber typsichere Art und Weise einen integrierten DSL mit der angegebenen Syntax zu erstellen.

+0

Ich weiß nicht, wie ich deine Antwort bis jetzt übersehen habe. Danke, dass du dir die Zeit genommen hast, eins aufzuschreiben :) –