Obecnie pracuję nad prostym tłumaczem dla języka programowania i mam taki typ danych:
data Expr
= Variable String
| Number Int
| Add [Expr]
| Sub Expr Expr
I mam wiele funkcji, które wykonują proste rzeczy, takie jak:
-- Substitute a value for a variable
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = go
where
go (Variable x)
| x == name = Number newValue
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
-- Replace subtraction with a constant with addition by a negative number
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = go
where
go (Sub x (Number y)) =
Add [go x, Number (-y)]
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
Ale w każdej z tych funkcji muszę powtórzyć część, która wywołuje kod rekurencyjnie, z niewielką zmianą w jednej części funkcji. Czy istnieje jakiś sposób, aby to zrobić bardziej ogólnie? Wolałbym nie kopiować i wklejać tej części:
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
I po prostu zmieniaj po jednym przypadku za każdym razem, ponieważ zduplikowanie takiego kodu wydaje się nieefektywne.
Jedyne rozwiązanie, jakie mogłem wymyślić, to mieć funkcję, która wywołuje funkcję najpierw w całej strukturze danych, a następnie rekurencyjnie w wyniku:
recurseAfter :: (Expr -> Expr) -> Expr -> Expr
recurseAfter f x =
case f x of
Add xs ->
Add $ map (recurseAfter f) xs
Sub x y ->
Sub (recurseAfter f x) (recurseAfter f y)
other -> other
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue =
recurseAfter $ \case
Variable x
| x == name -> Number newValue
other -> other
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd =
recurseAfter $ \case
Sub x (Number y) ->
Add [x, Number (-y)]
other -> other
Ale wydaje mi się, że prawdopodobnie powinien istnieć już prostszy sposób, aby to zrobić. Czy coś brakuje?
Add :: Expr -> Expr -> Expr
zamiastAdd :: [Expr] -> Expr
iSub
całkowicie się pozbądź .recurseAfter
jestana
w przebraniu. Możesz spojrzeć na anamorfizmy irecursion-schemes
. Biorąc to pod uwagę, myślę, że twoje ostateczne rozwiązanie jest tak krótkie, jak to tylko możliwe. Przejście na oficjalnerecursion-schemes
anamorfizmy niewiele zaoszczędzi.Odpowiedzi:
Gratulacje, właśnie odkryłeś anamorfizmy!
Oto twój kod, sformatowany tak, aby działał z
recursion-schemes
pakietem. Niestety, nie jest krótszy, ponieważ potrzebujemy trochę płyty kotłowej, aby maszyna mogła działać. (Może istnieć jakiś automatyczny sposób na uniknięcie płyty kotłowej, np. Użycie leków generycznych. Po prostu nie wiem.)Poniżej twój
recurseAfter
jest zastąpiony standardemana
.Najpierw określamy twój typ rekurencyjny, a także funktor, którego jest stałym punktem.
Następnie łączymy je z kilkoma instancjami, abyśmy mogli rozwinąć
Expr
się w izomorfięExprF Expr
i złożyć ją z powrotem.Na koniec dostosowujemy Twój oryginalny kod i dodajemy kilka testów.
Alternatywą może być
ExprF a
tylko zdefiniowanie , a następnie wyprowadzenietype Expr = Fix ExprF
. Oszczędza to część powyższego schematu (np. Dwa wystąpienia), kosztem użyciaFix (VariableF ...)
zamiast niegoVariable ...
, a także analogiczne dla innych konstruktorów.Można by dodatkowo złagodzić ten fakt, używając synonimów wzorców (jednak kosztem nieco więcej płyt grzewczych).
Aktualizacja: W końcu znalazłem narzędzie automagiczne, używając szablonu Haskell. To sprawia, że cały kod jest dość krótki. Zauważ, że
ExprF
funktor i dwie powyższe instancje wciąż istnieją pod maską i nadal musimy ich używać. Oszczędzamy tylko kłopotów z ich ręcznym definiowaniem, ale samo to oszczędza dużo wysiłku.źródło
Expr
wyraźnie określać , a nie coś w stylutype Expr = Fix ExprF
?Fix
+ prawdziwy konstruktor. Zastosowanie ostatniego podejścia z automatyzacją TH jest przyjemniejsze, IMO.Jako alternatywne podejście jest to również typowy przypadek użycia
uniplate
pakietu. Może używaćData.Data
generycznych zamiast szablonu Haskell do generowania płyty bazowej, więc jeśli wyprowadzaszData
instancje dlaExpr
:następnie
transform
funkcja fromData.Generics.Uniplate.Data
stosuje rekurencyjnie funkcję do każdego zagnieżdżonegoExpr
:Należy zauważyć, że w
replaceSubWithAdd
szczególności funkcjaf
jest napisana w celu wykonania nierekurencyjnego podstawienia;transform
sprawia, że jest rekurencyjnyx :: Expr
, więc robi tę samą magię dla funkcji pomocnika, coana
w odpowiedzi @ chi:To nie mniej niż rozwiązanie @ chi's Template Haskell. Jedną z potencjalnych zalet jest to, że
uniplate
zapewnia kilka dodatkowych funkcji, które mogą być pomocne. Na przykład, jeśli używaszdescend
zamiasttransform
, przekształca tylko bezpośrednie dzieci, które mogą dać ci kontrolę nad tym, gdzie ma miejsce rekurencja, lub możesz użyćrewrite
do ponownego przekształcenia wyniku transformacji, aż dojdziesz do stałego punktu. Jedną potencjalną wadą jest to, że „anamorfizm” brzmi o wiele fajniej niż „uniplate”.Pełny program:
źródło