2012-11-02 7 views
7

TL; DR: Ho bisogno di aiuto per capire come generare codice che restituirà uno di un piccolo numero di tipi di dati (probabilmente solo Double e Bool) da vari campi su record disparati.Come creare una DSL per cercare campi da un record in Haskell

forma lunga: Assumendo i seguenti tipi di dati

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

e un po 'di codice standard

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

Sto costruendo un piccolo DSL, e vogliono l'utente da scrivere qualcosa di simile al seguente

circle.origin 
square.side 

e sarà Codice enerate simile a

origin . circle 
side . square 

In analisi di questo, avrei le stringhe "cerchio" e "origine", per esempio. Ora ho bisogno di trasformarli in chiamate di funzione. Potrei ovviamente avere qualcosa di simile:

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 

e avere una funzione di ricerca per tipo di dati restituiti. Avere una funzione per tipo di dati è pratica dal punto di vista DSL in quanto si occuperà solo di 2 o 3 tipi di dati. Tuttavia, questo sembra quasi un modo particolarmente efficace di fare le cose. C'è un modo migliore (sicuramente) di fare questo? In caso contrario, esiste un modo per generare il codice per le varie funzioni di ricerca dai vari record da cui si desidera poter cercare i campi?

secondo luogo, c'è ancora la questione del analizzato "circle" o "square" dover chiamare la funzione circle o square appropriata. Se dovessi implementare questo utilizzando le classi di tipo, avrei potuto fare qualcosa di simile:

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

ma poi che mi lascia con la necessità di capire quale tipo di far rispettare sulla funzione di ricerca, e peggio di dover scrivere a mano istanze per ogni (di molti) record su cui voglio usare questo.

Nota: il fatto che Circle e Square possano essere rappresentati utilizzando un singolo ADT è secondario alla mia domanda in quanto si tratta di un esempio forzato. Il codice effettivo comporterà vari record molto diversi, di cui l'unica cosa che hanno in comune è avere campi dello stesso tipo.

+6

Vorrei esaminare il pacchetto Lens: http://hackage.haskell.org/package/lens-3.1 e anche su Vinyl https://github.com/jonsterling/Vinyl – ErikR

+1

+1 per 'lens'. Ha il supporto di Template Haskell per poter creare automaticamente obiettivi da tipi di dati. Quindi il tuo codice di analisi si limita a tradurre le stringhe negli obiettivi corrispondenti. –

+2

Sembra che tu stia incorporando il tuo DSL con una sorta di HOAS (sintassi astratta dell'ordine superiore) che utilizza le funzioni del linguaggio meta (Haskell) nel linguaggio incorporato. Se vuoi alcune note sull'utilizzo di HOAS, guarda il documento "Unembedding" di Robert Atkey e co-autore https://personal.cis.strath.ac.uk/robert.atkey/unembedding.html. Se HOAS non significa nulla per te, allora probabilmente stai meglio con la normale sintassi del primo ordine: rappresentano funzioni incorporate con un identificatore e valutano con un ambiente che contiene una ricerca delle funzioni primitive. –

risposta

1

Ho provato ad utilizzare Template Haskell per fornire un modo piacevole e sicuro per risolvere questo problema. Per fare questo, ho costruito le espressioni da una determinata stringa.

Suppongo che il pacchetto di lenti possa farlo, ma potrebbe essere una soluzione più semplice e più flessibile.

Può essere utilizzato in questo modo:

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

E si definisce in questo modo:

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

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

Quindi l'espressione risultato sarà: x (origin circle)

Nota: splitEvery è una funzione che divide una lista in sottoliste eliminando l'elemento dato. Esempio di implementazione:

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) 

Questo è un modo pesante ma sicuro per creare un DSL incorporato con la sintassi specificata.

+0

Non so quanto mi sia mancata di vedere la tua risposta fino ad ora. Grazie per aver dedicato del tempo per scriverne uno :) –

Problemi correlati