2012-11-02 6 views
7

TL; DR: Potrzebuję pomocy w ustaleniu, jak wygenerować kod, który zwróci jedną z niewielkiej liczby typów danych (prawdopodobnie tylko Double i Bool) z różnych dziedzin na różnych rekordach.Jak zbudować DSL do wyszukiwania pól z rekordu w Haskell

Długa forma: Przy założeniu następujących typów danych

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

i jakiś standardowy kod

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

Buduję małą DSL, a użytkownik chce się napisać coś jak po

circle.origin 
square.side 

i będzie Kod energetyczny podobny do

origin . circle 
side . square 

Podczas parsowania tego, mam na przykład łańcuchy "koło" i "pochodzenie". Muszę teraz przekształcić je w wywołania funkcji. Mogłem oczywiście mieć coś takiego:

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 

i mają jedną funkcję wyszukiwania dla każdego zwróconego typu danych. Posiadanie jednej funkcji na typ danych jest praktyczne z punktu widzenia DSL, ponieważ będzie to naprawdę dotyczyć tylko 2 lub 3 typów danych. Jednak nie wydaje się to szczególnie skutecznym sposobem robienia rzeczy. Czy istnieje lepszy sposób (z pewnością) na robienie tego? Jeśli nie, czy istnieje sposób, w jaki mogę wygenerować kod dla różnych funkcji wyszukiwania z różnych rekordów, z których chcę móc wyszukiwać pola?

Po drugie, jest jeszcze kwestia przeanalizowany "circle" lub "square" potrzeby wezwać odpowiednie circle lub square funkcję. Gdybym zaimplementować to za pomocą klas typu, mógłby zrobić coś takiego:

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

ale potem, że zostawia mnie z konieczności, aby dowiedzieć się, jaki rodzaj wymusić na funkcji przeglądowej, i gorszym konieczności pisania ręcznego wystąpienia dla każdego (z wielu) rekordów, które chcę użyć.

Uwaga: fakt, że Circle i Square może być reprezentowany przy użyciu pojedynczego ADT, jest dodatkowy w stosunku do mojego pytania, ponieważ jest to przykładowy przykład. Rzeczywisty kod będzie zawierał różne bardzo różne zapisy, z których jedyną wspólną cechą jest posiadanie pól tego samego rodzaju.

+6

Spojrzałbym na pakiet Lens: http://hackage.haskell.org/package/lens-3.1, a także na Vinyl https://github.com/jonsterling/Vinyl – ErikR

+1

+1 dla "obiektywu". Posiada wsparcie dla szablonu Haskell, dzięki czemu można automatycznie budować obiektywy z typów danych. Następnie Twój kod parsujący po prostu tłumaczy łańcuchy na odpowiadające im soczewki. –

+2

Wygląda na to, że osadzasz swoje DSL za pomocą jakiegoś HOAS (składnia abstrakcyjnego wyższego rzędu), która korzysta z funkcji meta-języka (Haskell) w języku osadzonym. Jeśli chcesz przeczytać kilka uwag na temat korzystania z HOAS, spójrz na artykuł "Unembedding" autorstwa Roberta Atkey'a i współautorów https://personal.cis.strath.ac.uk/robert.atkey/unembedding.html. Jeśli HOAS nic dla ciebie nie znaczy, to prawdopodobnie najlepiej będzie, jeśli użyjesz zwykłej składni abstrakcyjnej pierwszego rzędu - reprezentujesz wbudowane funkcje z identyfikatorem i ocenisz w środowisku zawierającym odnośnik do funkcji pierwotnych. –

Odpowiedz

1

Próbowałem używać szablonu Haskell, aby zapewnić miły i bezpieczny sposób na rozwiązanie tego problemu. Aby to zrobić, skonstruowałem wyrażenia z danego ciągu znaków.

Przypuszczam, że pakiet soczewek może to zrobić, ale może być prostszym i bardziej elastycznym rozwiązaniem.

Może być stosowany tak:

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

i jest zdefiniowany następująco:

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

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

Więc wyrażenie Wynikiem będzie: x (origin circle)

Uwaga: splitEvery to funkcja dzieli listę na podlisty, odbierając dany element. Przykładowa implementacja:

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) 

Jest to ciężki, ale bezpieczny w użyciu sposób tworzenia osadzonego DSL o podanej składni.

+0

Nie wiem, jak przegapiłem twoją odpowiedź do tej pory. Dziękuję za poświęcenie czasu na napisanie jednego :) –