Навигация и модификация АСТ, построенных на Свободной монаде в Хаскелле
Я пытаюсь структурировать AST, используя Free monad, основываясь на некоторой полезной литературе, которую я читал в Интернете.
У меня есть некоторые вопросы о работе с этими типами АСТ на практике, которые я рассмотрел в следующем примере.
Предположим, что мой язык допускает следующие команды:
{-# LANGUAGE DeriveFunctor #-}
data Command next
= DisplayChar Char next
| DisplayString String next
| Repeat Int (Free Command ()) next
| Done
deriving (Eq, Show, Functor)
и я определяю шаблон Free monad вручную:
displayChar :: Char -> Free Command ()
displayChar ch = liftF (DisplayChar ch ())
displayString :: String -> Free Command ()
displayString str = liftF (DisplayString str ())
repeat :: Int -> Free Command () -> Free Command ()
repeat times block = liftF (Repeat times block ())
done :: Free Command r
done = liftF Done
который позволяет мне указывать следующие программы:
prog :: Free Command r
prog =
do displayChar 'A'
displayString "abc"
repeat 5 $
displayChar 'Z'
displayChar '\n'
done
Теперь я хочу выполнить свою программу, которая кажется достаточно простой.
execute :: Free Command r -> IO ()
execute (Free (DisplayChar ch next)) = putChar ch >> execute next
execute (Free (DisplayString str next)) = putStr str >> execute next
execute (Free (Repeat n block next)) = forM_ [1 .. n] (\_ -> execute block) >> execute next
execute (Free Done) = return ()
execute (Pure r) = return ()
и
λ> execute prog
AabcZZZZZ
Хорошо. Это все хорошо, но теперь я хочу узнать о моем АСТ и выполнить на нем преобразования. Подумайте, как оптимизация в компиляторе.
Здесь простой: если блок Repeat
содержит только команды DisplayChar
, то я хотел бы заменить все это на DisplayString
. Другими словами,
Я хотел бы преобразовать repeat 2 (displayChar 'A' >> displayChar 'B')
с помощью displayString "ABAB"
.
Здесь моя попытка:
optimize [email protected](Free (Repeat n block next)) =
if all isJust charsToDisplay then
let chars = catMaybes charsToDisplay
in
displayString (concat $ replicate n chars) >> optimize next
else
c >> optimize next
where
charsToDisplay = project getDisplayChar block
optimize (Free (DisplayChar ch next)) = displayChar ch >> optimize next
optimize (Free (DisplayString str next)) = displayString str >> optimize next
optimize (Free Done) = done
optimize [email protected](Pure r) = c
getDisplayChar (Free (DisplayChar ch _)) = Just ch
getDisplayChar _ = Nothing
project :: (Free Command a -> Maybe u) -> Free Command a -> [Maybe u]
project f = maybes
where
maybes (Pure a) = []
maybes [email protected](Free cmd) =
let build next = f c : maybes next
in
case cmd of
DisplayChar _ next -> build next
DisplayString _ next -> build next
Repeat _ _ next -> build next
Done -> []
Наблюдение АСТ в GHCI показывает, что это работает правильно, и действительно
λ> optimize $ repeat 3 (displayChar 'A' >> displayChar 'B')
Free (DisplayString "ABABAB" (Pure ()))
λ> execute . optimize $ prog
AabcZZZZZ
λ> execute prog
AabcZZZZZ
Но я не доволен. На мой взгляд, этот код повторяется. Я должен определить, как проходить через мой AST каждый раз, когда я хочу его изучить, или определить такие функции, как my project
, которые дают мне представление о нем. Я должен сделать то же самое, когда хочу изменить дерево.
Итак, мой вопрос: этот подход мой единственный вариант? Могу ли я сопоставлять шаблоны с моим АСТ, не имея дело с тоннами гнездования? Могу ли я перемещаться по дереву последовательным и общим образом (возможно, молния, или траверс, или что-то еще)? Какие подходы обычно принимаются здесь?
Весь файл находится ниже:
{-# LANGUAGE DeriveFunctor #-}
module Main where
import Prelude hiding (repeat)
import Control.Monad.Free
import Control.Monad (forM_)
import Data.Maybe (catMaybes, isJust)
main :: IO ()
main = execute prog
prog :: Free Command r
prog =
do displayChar 'A'
displayString "abc"
repeat 5 $
displayChar 'Z'
displayChar '\n'
done
optimize [email protected](Free (Repeat n block next)) =
if all isJust charsToDisplay then
let chars = catMaybes charsToDisplay
in
displayString (concat $ replicate n chars) >> optimize next
else
c >> optimize next
where
charsToDisplay = project getDisplayChar block
optimize (Free (DisplayChar ch next)) = displayChar ch >> optimize next
optimize (Free (DisplayString str next)) = displayString str >> optimize next
optimize (Free Done) = done
optimize [email protected](Pure r) = c
getDisplayChar (Free (DisplayChar ch _)) = Just ch
getDisplayChar _ = Nothing
project :: (Free Command a -> Maybe u) -> Free Command a -> [Maybe u]
project f = maybes
where
maybes (Pure a) = []
maybes [email protected](Free cmd) =
let build next = f c : maybes next
in
case cmd of
DisplayChar _ next -> build next
DisplayString _ next -> build next
Repeat _ _ next -> build next
Done -> []
execute :: Free Command r -> IO ()
execute (Free (DisplayChar ch next)) = putChar ch >> execute next
execute (Free (DisplayString str next)) = putStr str >> execute next
execute (Free (Repeat n block next)) = forM_ [1 .. n] (\_ -> execute block) >> execute next
execute (Free Done) = return ()
execute (Pure r) = return ()
data Command next
= DisplayChar Char next
| DisplayString String next
| Repeat Int (Free Command ()) next
| Done
deriving (Eq, Show, Functor)
displayChar :: Char -> Free Command ()
displayChar ch = liftF (DisplayChar ch ())
displayString :: String -> Free Command ()
displayString str = liftF (DisplayString str ())
repeat :: Int -> Free Command () -> Free Command ()
repeat times block = liftF (Repeat times block ())
done :: Free Command r
done = liftF Done
Ответы
Ответ 1
Здесь мой прием с использованием syb (как упоминалось в Reddit):
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Main where
import Prelude hiding (repeat)
import Data.Data
import Control.Monad (forM_)
import Control.Monad.Free
import Control.Monad.Free.TH
import Data.Generics (everywhere, mkT)
data CommandF next = DisplayChar Char next
| DisplayString String next
| Repeat Int (Free CommandF ()) next
| Done
deriving (Eq, Show, Functor, Data, Typeable)
makeFree ''CommandF
type Command = Free CommandF
execute :: Command () -> IO ()
execute = iterM handle
where
handle = \case
DisplayChar ch next -> putChar ch >> next
DisplayString str next -> putStr str >> next
Repeat n block next -> forM_ [1 .. n] (\_ -> execute block) >> next
Done -> return ()
optimize :: Command () -> Command ()
optimize = optimize' . optimize'
where
optimize' = everywhere (mkT inner)
inner :: Command () -> Command ()
-- char + char becomes string
inner (Free (DisplayChar c1 (Free (DisplayChar c2 next)))) = do
displayString [c1, c2]
next
-- char + string becomes string
inner (Free (DisplayChar c (Free (DisplayString s next)))) = do
displayString $ c : s
next
-- string + string becomes string
inner (Free (DisplayString s1 (Free (DisplayString s2 next)))) = do
displayString $ s1 ++ s2
next
-- Loop unrolling
inner [email protected](Free (Repeat n block next)) | n < 5 = forM_ [1 .. n] (\_ -> block) >> next
| otherwise = f
inner a = a
prog :: Command ()
prog = do
displayChar 'a'
displayChar 'b'
repeat 1 $ displayChar 'c' >> displayString "def"
displayChar 'g'
displayChar 'h'
repeat 10 $ do
displayChar 'i'
displayChar 'j'
displayString "klm"
repeat 3 $ displayChar 'n'
main :: IO ()
main = do
putStrLn "Original program:"
print prog
putStrLn "Evaluation of original program:"
execute prog
putStrLn "\n"
let opt = optimize prog
putStrLn "Optimized program:"
print opt
putStrLn "Evaluation of optimized program:"
execute opt
putStrLn ""
Вывод:
$ cabal exec runhaskell ast.hs
Original program:
Free (DisplayChar 'a' (Free (DisplayChar 'b' (Free (Repeat 1 (Free (DisplayChar 'c' (Free (DisplayString "def" (Pure ()))))) (Free (DisplayChar 'g' (Free (DisplayChar 'h' (Free (Repeat 10 (Free (DisplayChar 'i' (Free (DisplayChar 'j' (Free (DisplayString "klm" (Pure ()))))))) (Free (Repeat 3 (Free (DisplayChar 'n' (Pure ()))) (Pure ()))))))))))))))
Evaluation of original program:
abcdefghijklmijklmijklmijklmijklmijklmijklmijklmijklmijklmnnn
Optimized program:
Free (DisplayString "abcdefgh" (Free (Repeat 10 (Free (DisplayString "ijklm" (Pure ()))) (Free (DisplayString "nnn" (Pure ()))))))
Evaluation of optimized program:
abcdefghijklmijklmijklmijklmijklmijklmijklmijklmijklmijklmnnn
Возможно, можно избавиться от * Free * s с использованием синонимов GHC 7.8, но по какой-то причине вышеуказанный код работает только с использованием GHC 7.6, кажется, что экземпляр данных Free отсутствует. Должен заглянуть в это...
Ответ 2
Если ваша проблема связана с шаблоном, вы не обойдетесь, если используете Free
! Вы всегда будете придерживаться дополнительного конструктора на каждом уровне.
Но с другой стороны, если вы используете Free
, у вас есть очень простой способ обобщить рекурсию по вашей структуре данных. Вы можете написать все это с нуля, но я использовал пакет recursion-schemes
:
import Data.Functor.Foldable
data (:+:) f g a = L (f a) | R (g a) deriving (Functor, Eq, Ord, Show)
type instance Base (Free f a) = f :+: Const a
instance (Functor f) => Foldable (Free f a) where
project (Free f) = L f
project (Pure a) = R (Const a)
instance Functor f => Unfoldable (Free f a) where
embed (L f) = Free f
embed (R (Const a)) = Pure a
instance Functor f => Unfoldable (Free f a) where
embed (L f) = Free f
embed (R (Const a)) = Pure a
Если вы не знакомы с этим (прочитайте документацию), но в основном все, что вам нужно знать, это project
принимает некоторые данные, такие как Free f a
и "un-nests" на одном уровне, производя что-то вроде (f :+: Const a) (Free f a)
. Теперь вы предоставляете регулярные функции, такие как fmap
, Data.Foldable.foldMap
и т.д., Доступ к структуре ваших данных, поскольку аргумент функтора является поддеревом.
Выполнение очень просто, хотя и не намного более кратким:
execute :: Free Command r -> IO ()
execute = cata go where
go (L (DisplayChar ch next)) = putChar ch >> next
go (L (DisplayString str next)) = putStr str >> next
go (L (Repeat n block next)) = forM_ [1 .. n] (const $ execute block) >> next
go (L Done) = return ()
go (R _) = return ()
Однако упрощение становится намного проще. Мы можем определить упрощение по всем типам данных, которые имеют экземпляры Foldable
и Unfoldable
:
reduce :: (Foldable t, Functor (Base t), Unfoldable t) => (t -> Maybe t) -> t -> t
reduce rule x = let y = embed $ fmap (reduce rule) $ project x in
case rule y of
Nothing -> y
Just y' -> y'
Правило упрощения должно только упростить один уровень АСТ (а именно, самый верхний уровень). Тогда, если упрощение может применяться к подструктуре, оно также выполнит его там. Обратите внимание, что выше reduce
работает снизу вверх; вы также можете уменьшить сверху вниз:
reduceTD :: (Foldable t, Functor (Base t), Unfoldable t) => (t -> Maybe t) -> t -> t
reduceTD rule x = embed $ fmap (reduceTD rule) $ project y
where y = case rule x of
Nothing -> x
Just x' -> x'
Ваше правило упрощения примера может быть написано очень просто:
getChrs :: (Command :+: Const ()) (Maybe String) -> Maybe String
getChrs (L (DisplayChar c n)) = liftA (c:) n
getChrs (L Done) = Just []
getChrs (R _) = Just []
getChrs _ = Nothing
optimize (Free (Repeat n dc next)) = do
chrs <- cata getChrs dc
return $ Free $ DisplayString (concat $ map (replicate n) chrs) next
optimize _ = Nothing
Из-за того, как вы определили свой тип данных, у вас нет доступа ко 2-му аргументу Repeat
, поэтому для таких вещей, как repeat' 5 (repeat' 3 (displayChar 'Z')) >> done
, внутренний Repeat
не может быть упрощен. Если это ситуация, с которой вы собираетесь столкнуться, вы либо изменяете свой тип данных, либо принимаете гораздо больше шаблонов, либо записываете исключение:
reduceCmd rule (Free (Repeat n c r)) =
let x = Free (Repeat n (reduceCmd rule c) (reduceCmd rule r)) in
case rule x of
Nothing -> x
Just x' -> x'
reduceCmd rule x = embed $ fmap (reduceCmd rule) $ project x
Использование recursion-schemes
или тому подобное, вероятно, сделает ваш код более легко расширяемым. Но это не нужно никаким способом:
execute = iterM go where
go (DisplayChar ch next) = putChar ch >> next
go (DisplayString str next) = putStr str >> next
go (Repeat n block next) = forM_ [1 .. n] (const $ execute block) >> next
go Done = return ()
getChrs
не может получить доступ к Pure
, и ваши программы будут иметь форму Free Command ()
, поэтому перед тем, как вы его примените, вы должны заменить ()
на Maybe String
.
getChrs :: Command (Maybe String) -> Maybe String
getChrs (DisplayChar c n) = liftA (c:) n
getChrs (DisplayString s n) = liftA (s++) n
getChrs Done = Just []
getChrs _ = Nothing
optimize :: Free Command a -> Maybe (Free Command a)
optimize (Free (Repeat n dc next)) = do
chrs <- iter getChrs $ fmap (const $ Just []) dc
return $ Free $ DisplayString (concat $ map (replicate n) chrs) next
optimize _ = Nothing
Обратите внимание, что reduce
почти то же самое, что и раньше, за исключением двух вещей: project
и embed
заменяются на соответствие шаблонам на Free
и Free
соответственно; и вам нужен отдельный случай для Pure
. Это должно сказать вам, что Foldable
и Unfoldable
обобщают вещи, которые выглядят "Free
.
reduce
:: Functor f =>
(Free f a -> Maybe (Free f a)) -> Free f a -> Free f a
reduce rule (Free x) = let y = Free $ fmap (reduce rule) $ x in
case rule y of
Nothing -> y
Just y' -> y'
reduce rule [email protected](Pure _) = case rule a of
Nothing -> a
Just b -> b
Все остальные функции изменены аналогичным образом.
Ответ 3
Пожалуйста, не думайте о молниях, обходах, SYB или объективе, пока не воспользуетесь стандартными функциями Free
. Ваши execute
, optimize
и project
являются стандартными бесплатными схемами рекурсии монады, которые уже доступны в пакете:
optimize :: Free Command a -> Free Command a
optimize = iterM $ \f -> case f of
[email protected](Repeat n block next) ->
let charsToDisplay = project getDisplayChar block in
if all isJust charsToDisplay then
let chars = catMaybes charsToDisplay in
displayString (concat $ replicate n chars) >> next
else
liftF c >> next
DisplayChar ch next -> displayChar ch >> next
DisplayString str next -> displayString str >> next
Done -> done
getDisplayChar :: Command t -> Maybe Char
getDisplayChar (DisplayChar ch _) = Just ch
getDisplayChar _ = Nothing
project' :: (Command [u] -> u) -> Free Command [u] -> [u]
project' f = iter $ \c -> f c : case c of
DisplayChar _ next -> next
DisplayString _ next -> next
Repeat _ _ next -> next
Done -> []
project :: (Command [u] -> u) -> Free Command a -> [u]
project f = project' f . fmap (const [])
execute :: Free Command () -> IO ()
execute = iterM $ \f -> case f of
DisplayChar ch next -> putChar ch >> next
DisplayString str next -> putStr str >> next
Repeat n block next -> forM_ [1 .. n] (\_ -> execute block) >> next
Done -> return ()
Поскольку ваши компоненты имеют не более одного продолжения, вы, вероятно, можете найти умный способ избавиться от всех этих >> next
.
Ответ 4
Вы можете сделать это проще. Еще предстоит проделать определенную работу, потому что он не будет выполнять полную оптимизацию в первый проход, но после двух проходов он полностью оптимизирует вашу примерную программу. Я оставлю это упражнение до вас, но в противном случае вы можете сделать это очень просто с помощью сопоставления шаблонов при оптимизации, которую вы хотите сделать. Он все еще немного повторяется, но устраняет много осложнений, которые у вас были:
optimize (Free (Repeat n block next)) = optimize (replicateM n block >> next)
optimize (Free (DisplayChar ch1 (Free (DisplayChar ch2 next)))) = optimize (displayString [ch1, ch2] >> next)
optimize (Free (DisplayChar ch (Free (DisplayString str next)))) = optimize (displayString (ch:str) >> next)
optimize (Free (DisplayString s1 (Free (DisplayString s2 next)))) = optimize (displayString (s1 ++ s2) >> next)
optimize (Free (DisplayString s (Free (DisplayChar ch next)))) = optimize (displayString (s ++ [ch]) >> next)
optimize (Free (DisplayChar ch next)) = displayChar ch >> optimize next
optimize (Free (DisplayString str next)) = displayString str >> optimize next
optimize (Free Done) = done
optimize [email protected](Pure r) = c
Все, что я сделал, это совпадение с шаблоном на repeat n (displayChar c)
, displayChar c1 >> displayChar c2
, displayChar c >> displayString s
, displayString s >> displayChar c
и displayString s1 >> displayString s2
. Есть и другие оптимизации, которые можно сделать, но это было довольно легко и не зависело от сканирования всего остального, просто итеративно перешагнув рекурсивно оптимизирующий алгоритм.