Эта заметка о схемах рекурсии для бифункоторов. Тем, кто не знаком со схемами рекурсии, рекомендую для начала прочитать цикл статей в блоге sumtypeofway.
Для примера напишем простой интерпретатор для следующего языка:
data Stmt
= Assign String Expr
| Print Expr
| Expr Expr
| Seq (NonEmpty Stmt)
data Expr
= Var String
| Val Int
| Binop Binop Expr Expr
| If Relop Expr Expr Expr (Maybe Expr)
| ESeq Stmt Expr
data Binop = Add | Sub | Mul | Div
data Relop = Eq | Ne | Lt | Le | Gt | Ge
Так как у нас два взаимнорекурсивных типа, мы не можем использовать уже знакомый тип Fix:
newtype Fix f = Fix { unFix :: f (Fix f) }
Поэтому напишем тип Fix2 для такого случая, а также базовые функторы для Stmt и Expr и инстансы класса Bifunctor для них:
{-# LANGUAGE LambdaCase #-}
import Control.Exception (Exception, handle, throwIO)
import Control.Monad (void)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (ReaderT, asks, runReaderT)
import Data.Bifunctor (Bifunctor (..))
import Data.Functor (($>))
import Data.IORef (IORef, modifyIORef, newIORef,
readIORef)import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Map.Strict (Map)
import Data.Maybe (fromMaybe)
import Prelude hiding (print, seq)
import qualified Data.Map.Strict as Map
import qualified Prelude
data StmtF stmt expr
= Assign String expr
| Print expr
| Expr expr
| Seq (NonEmpty stmt)
deriving Functor
instance Bifunctor StmtF where
= \case
bimap f g Assign var e -> Assign var (g e)
Print e -> Print (g e)
Expr e -> Expr (g e)
Seq ss -> Seq $ f <$> ss
data ExprF expr stmt
= Var String
| Val Int
| Binop Binop expr expr
| If Relop expr expr expr (Maybe expr)
| ESeq stmt expr
deriving Functor
instance Bifunctor ExprF where
= \case
bimap f g Var var -> Var var
Val val -> Val val
Binop op e1 e2 -> Binop op (f e1) (f e2)
If op e1 e2 th mel -> If op (f e1) (f e2) (f th) (f <$> mel)
ESeq s e -> ESeq (g s) (f e)
data Binop = Add | Sub | Mul | Div
data Relop = Eq | Ne | Lt | Le | Gt | Ge
Как можно видеть, Fix2 принимает на вход два бифунктора — f и g. f — это тип, значение которого мы хотим получить, а g — тип, который передаётся в качестве параметра в f. Так, если мы ходим получить Stmt, мы сначала передаём StmtF, а потом ExprF. Если же мы хотим получить Expr, делаем наоборот. Обращу внимание, что во втором аргументе f мы меняем местами f и g, потому что первый параметр f нужен для вложенности по f, а второй — для вложенности по g.
newtype Fix2 f g = Fix2 { unFix2 :: f (Fix2 f g) (Fix2 g f) }
Теперь напишем свёртку для Fix2:
cata2 :: (Bifunctor f, Bifunctor g)
-- Алгебра для f
=> (f a b -> a)
-- Алгебра для g
-> (g b a -> b)
-> Fix2 f g
-> a
= f . bimap (cata2 f g) (cata2 g f) . unFix2 cata2 f g
Также напишем хистоморфизм:
data Cofree2 f g a b = a :< f (Cofree2 f g a b) (Cofree2 g f b a)
histo2 :: (Bifunctor f, Bifunctor g)
-- Алгебра для f
=> (f (Cofree2 f g a b) (Cofree2 g f b a) -> a)
-- Алгебра для g
-> (g (Cofree2 g f b a) (Cofree2 f g a b) -> b)
-> Fix2 f g
-> a
= let a :< _ = go1 fix2 in a
histo2 f g fix2 where go1 = (\x -> f x :< x) . bimap go1 go2 . unFix2
= (\x -> g x :< x) . bimap go2 go1 . unFix2 go2
Как видно, почти те же самые определения, что и для обычного Fix. Остальные свёртки и развёртки можно написать по аналогии. Теперь приступим к написанию самого интерпретатора:
type InterpretM a = ReaderT Context IO a
newtype Context = Context { ctxVars :: IORef (Map String Int) }
data InterpretError
= UndefinedVariable String
| ZeroDivision
deriving Show
instance Exception InterpretError where
runInterpreter :: Fix2 StmtF ExprF -> IO (Maybe InterpretError)
= do
runInterpreter stmt <- Context <$> newIORef Map.empty
ctx let result = cata2 runStmtF runExprF stmt
pure . Just) (runReaderT result ctx $> Nothing) handle (
runStmtF :: StmtF (InterpretM ()) (InterpretM Int) -> InterpretM ()
= \case
runStmtF Assign var mVal -> do
<- mVal
val <- asks ctxVars
varsRef $ modifyIORef varsRef (Map.insert var val)
liftIO Print mVal -> mVal >>= liftIO . Prelude.print
Expr e -> void $ e
Seq ss -> sequence_ ss
runExprF :: ExprF (InterpretM Int) (InterpretM ()) -> InterpretM Int
= \case
runExprF Var var -> do
<- asks ctxVars
varsRef <$> liftIO (readIORef varsRef)
Map.lookup var >>= maybe (liftIO $ throwIO $ UndefinedVariable var) pure
Val val -> pure val
Binop op mVal1 mVal2 -> do
<- mVal1
val1 <- mVal2
val2 case op of
Add -> pure (val1 + val2)
Sub -> pure (val1 - val2)
Mul -> pure (val1 * val2)
Div
| val2 == 0 -> liftIO $ throwIO $ ZeroDivision
| otherwise -> pure (val1 `div` val2)
If op me1 me2 mth mel -> do
<- me1
e1 <- me2
e2 if runRelop op e1 e2
then mth
else fromMaybe (pure 0) mel
ESeq s mVal -> s >> mVal
runRelop :: Relop -> Int -> Int -> Bool
= \case
runRelop Eq -> (==)
Ne -> (/=)
Lt -> (<)
Le -> (<=)
Gt -> (>)
Ge -> (>=)
Интерпретатор готов, осталось лишь написать выражения, которые мы будем интерпретировать. Для начала определим вспомогательные функции для конструирования наших выражений:
assign :: String -> Fix2 ExprF StmtF -> Fix2 StmtF ExprF
= Fix2 $ Assign var val assign var val
print :: Fix2 ExprF StmtF -> Fix2 StmtF ExprF
print = Fix2 . Print
expr :: Fix2 ExprF StmtF -> Fix2 StmtF ExprF
= Fix2 . Expr expr
seq :: NonEmpty (Fix2 StmtF ExprF) -> Fix2 StmtF ExprF
seq = Fix2 . Seq
var_ :: String -> Fix2 ExprF StmtF
= Fix2 . Var var_
val_ :: Int -> Fix2 ExprF StmtF
= Fix2 . Val val_
binop :: Binop
-> Fix2 ExprF StmtF
-> Fix2 ExprF StmtF
-> Fix2 ExprF StmtF
= Fix2 $ Binop op e1 e2 binop op e1 e2
if_ :: Relop
-> Fix2 ExprF StmtF
-> Fix2 ExprF StmtF
-> Fix2 ExprF StmtF
-> Maybe (Fix2 ExprF StmtF)
-> Fix2 ExprF StmtF
= Fix2 $ If op e1 e2 th mel if_ op e1 e2 th mel
eseq :: Fix2 StmtF ExprF -> Fix2 ExprF StmtF -> Fix2 ExprF StmtF
= Fix2 $ ESeq s e eseq s e
Теперь напишем сами выражения:
stmt1 :: Fix2 StmtF ExprF
= seq $ assign "a" (val_ 12)
stmt1 :| [ assign "b" (binop Mul (binop Add (var_ "a") (val_ 13))
"a"))
(var_ $ if_ Gt (var_ "b") (val_ 187)
, expr print (var_ "b")) (val_ 0))
(eseq (Just $
(seq $ (assign "b"
eseq (Div (var_ "b")
(binop 10)))
(val_ :| [print (var_ "b")])
0))
(val_ ]
stmt2 :: Fix2 StmtF ExprF
= seq $ assign "a" (val_ 0)
stmt2 :| [ assign "b" (binop Div (val_ 12) (var_ "a"))
print (var_ "b")
, ]
stmt3 :: Fix2 StmtF ExprF
= seq $ assign "a" (val_ 12)
stmt3 :| [ assign "b" (binop Mul (var_ "a") (var_ "c"))
print (var_ "b")
, ]
Далее выполним их интерпретацию:
runAndShowError :: Fix2 StmtF ExprF -> IO ()
= runInterpreter s >>= \case
runAndShowError s Nothing -> pure ()
Just err -> Prelude.print err
main :: IO ()
= mapM_ runAndShowError [stmt1, stmt2, stmt3] main
После запуска в консоль будут напечатаны следующие строки:
300
ZeroDivision
UndefinedVariable "c"