导航和修改在Haskell中的Free monad上构建的AST

  发布于 2023-01-09 17:17

我试图根据我在网上阅读的一些有用的文献,使用Free monad构建AST.

我有一些关于在实践中使用这些AST的问题,我已经归结为以下示例.

假设我的语言允许以下命令:

{-# 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

好的.这一切都很好,但现在我想了解我的AST,并对其执行转换.想想编译器中的优化.

这里有一个简单的例子:如果一个Repeat块只包含DisplayChar命令,那么我想用适当的替换整个东西DisplayString.换句话说,我想变换repeat 2 (displayChar 'A' >> displayChar 'B')displayString "ABAB".

这是我的尝试:

optimize c@(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 c@(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 c@(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中的AST表明这是正确的,实际上

?> optimize $ repeat 3 (displayChar 'A' >> displayChar 'B')
Free (DisplayString "ABABAB" (Pure ()))


?> execute . optimize $ prog
AabcZZZZZ
?> execute prog
AabcZZZZZ 

但我不开心.在我看来,这段代码是重复的.每次我想要检查它时,我必须定义如何遍历我的AST,或者定义像我project这样的函数给我一个视图.当我想修改树时,我必须做同样的事情.

所以,我的问题是:这种方法是我唯一的选择吗?我可以在我的AST上进行模式匹配而不需要处理大量的嵌套吗?我可以以一致且通用的方式遍历树(可能是Zippers,Traversable,还是其他东西)?这里通常采取什么方法?

整个文件如下:

{-# 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 c@(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 c@(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 c@(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

user2407038.. 10

如果您的问题是使用样板,如果您使用,则不会绕过它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,并将其"展开"一级,产生类似的东西(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 ()

但是,简化变得更容易.我们可以定义具有和实例的所有数据类型的简化:FoldableUnfoldable

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' 

简化规则只需要简化AST的一个级别(即最顶层).然后,如果简化可以应用于子结构,它也将在那里执行.注意上面的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

由于您定义数据类型的方式,您无法访问第二个争论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几乎与以前完全相同,除了两件事:projectembed分别用on Free和pattern匹配替换Free; 你需要一个单独的案例Pure.这应该告诉你FoldableUnfoldable概括"看起来像"的东西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 a@(Pure _) = case rule a of 
                           Nothing -> a
                           Just  b -> b 

所有其他功能都进行了类似的修改.

3 个回答
  • 如果您的问题是使用样板,如果您使用,则不会绕过它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,并将其"展开"一级,产生类似的东西(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 ()
    

    但是,简化变得更容易.我们可以定义具有和实例的所有数据类型的简化:FoldableUnfoldable

    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' 
    

    简化规则只需要简化AST的一个级别(即最顶层).然后,如果简化可以应用于子结构,它也将在那里执行.注意上面的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
    

    由于您定义数据类型的方式,您无法访问第二个争论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几乎与以前完全相同,除了两件事:projectembed分别用on Free和pattern匹配替换Free; 你需要一个单独的案例Pure.这应该告诉你FoldableUnfoldable概括"看起来像"的东西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 a@(Pure _) = case rule a of 
                               Nothing -> a
                               Just  b -> b 
    

    所有其他功能都进行了类似的修改.

    2023-01-09 17:17 回答
  • 这是我使用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 f@(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
    

    有可能使用GHC 7.8 模式同义词去掉*Free*s ,但由于某种原因,上面的代码只能使用GHC 7.6,FreeData实例似乎缺失了.应该看看...

    2023-01-09 17:19 回答
  • 在您充分利用标准功能之前,请不要考虑拉链,遍历,SYB或镜头Free.您的execute,optimize并且project只是标准的免费monad递归方案,已在包中提供:

    optimize :: Free Command a -> Free Command a
    optimize = iterM $ \f -> case f of
      c@(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.

    2023-01-09 17:21 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有