Используйте нотацию do монады Haskell для определения синтаксического дерева

Я пытаюсь построить абстрактное синтаксическое дерево, которое позволяет определение с использованием нотации монады do следующим образом:

ast = do
    Variable uint8 "i"
    Function Void "f" $ do
        Variable uint8 "local_y"
        Comment "etc. etc."

Показанная здесь конструкция была взята из Text.Blaze.Html, где он используется для определения дерева HTML.

Вопросы разбросаны по всему тексту ниже. Главный вопрос в том, как это сделать правильно. Разумеется, любой вклад, помогающий понять эту конструкцию, приветствуется.

Итак, прежде всего, вот небольшой, ущербный, но «рабочий» пример. Это синтаксическое дерево с объявлениями переменных и функций определенного типа, строками комментариев и объявлением-заполнителем, которое используется для замен:

{-# LANGUAGE ExistentialQuantification #-}
module Question
where

import           Control.Applicative
import           Data.Monoid         (Monoid, (<>))
import           Data.String.Utils   (rstrip)

type NumberOfBits = Word
type VariableName = String

data Type = UInt NumberOfBits
          | Int NumberOfBits
          | Void

uint8 = UInt 8
int8 = Int 8

instance Show Type where
    show (UInt w) = "uint" <> show w
    show (Int w)  = "int" <> show w
    show Void     = "void"

data TreeM a = Variable Type VariableName            -- variable declaration
             | Function Type VariableName (TreeM a)  -- function declaration
             | Comment String                        -- a comment
             | PlaceHolder String                    -- a placeholder with                  
             | forall b. Append (TreeM b) (TreeM a)  -- combiner
             | Empty a                               -- needed for what?

type Tree = TreeM ()

subTreeOf :: TreeM a -> a
subTreeOf (Variable _ _)   = undefined
subTreeOf (Function _ _ t) = subTreeOf t
subTreeOf (Comment _)      = undefined
subTreeOf (Empty t)        = t

instance Monoid a => Monoid (TreeM a) where
    mempty = Empty mempty
    mappend = Append
    mconcat = foldr Append mempty

instance Functor TreeM where
    fmap f x = x `Append` (Empty (f (subTreeOf x))) -- fmap :: (a -> b) -> f a -> f b

instance Applicative TreeM where
    pure x = Empty x
    (<*>) x y = (x `Append` y) `Append` (Empty (subTreeOf x (subTreeOf y)))  -- (<*>) :: f (a -> b) -> f a -> f b
    (*>) = Append

instance Monad TreeM where
    return x = Empty x
    (>>) = Append             -- not really needed: (>>) would default to (*>)
    t >>= f = t `Append` (f (subTreeOf t))

indent :: String -> String
indent s = rstrip $ unlines $ map ("    "<>) (lines s)

render :: TreeM a -> String
render (Variable y n)   = "Variable " <> (show y) <> " " <> show n
render (Function r n t) = "Function" <> " " <> n <> " returning " <> (show r) <> ":\n" <> indent (render t)
render (PlaceHolder n)  = "Placeholder \"" <> n <> "\""
render (Append t t')    = (render t) <> "\n" <> (render t')
render (Empty _)        = ""

-- |In input tree t substitute a PlaceHolder of name n' with the Tree t'
sub :: TreeM a -> (String, TreeM a) -> TreeM a
sub t@(PlaceHolder n) (n', t') = if n == n' then t' else t
sub (Function y n t) s         = Function y n (sub t s)
--sub (Append t t') s            = Append (sub t s) (sub t' s)  -- Error!
sub t _                        = t

code :: Tree
code = do
    Variable uint8 "i"
    Variable int8 "j"
    Function Void "f" $ do
        Comment "my function f"
        Variable int8 "i1"
        Variable int8 "i2"
    PlaceHolder "the_rest"

main :: IO ()
main = do
    putStrLn $ render code
    putStrLn "\nNow apply substitution:\n"
    putStrLn $ render (sub code ("the_rest", Comment "There is nothing here"))

Это (должен быть) очень удобный способ определения сложных древовидных структур. В частности, это должен быть синтаксически наименее зашумленный и удобный для пользователя способ определения синтаксического дерева.

В общем, я изо всех сил пытаюсь понять точное значение a в TreeM a. Насколько я понимаю, a может быть любого из типов Variable, Function, PlaceHolder и т. д.

Отмечу несколько вещей, которые кажутся мне странными:

  1. В forall b. Append (TreeM b) (TreeM a) порядок аргументов TreeM a и TreeM b для Append кажется обратным. В любом случае использование квантора существования в суммовом типе выглядит странно. Если я правильно понимаю, он определяет семейство конструкторов для TreeM.
  2. Из всех функций, требуемых Functor, Applicative и Monad, реально используется только монада >>. (Это указывает на то, что свободная монада может быть подходящим инструментом для этой работы.) На самом деле мне никогда не приходило в голову, что нотация do использует оператор >> и что этот факт можно использовать.
  3. undefined необходимо использовать в subTreeOf, чтобы сделать функцию общей.

Как уже отмечалось, приведенный выше пример ошибочен: части конструкции не подходят для AST:

  1. Определение Empty имеет смысл для деревьев HTML, оно используется для пустых тегов, таких как <br />. Но для АСТ это не имеет смысла. Он был оставлен как есть, чтобы реализации Applicative и Functor работали.
  2. Точно так же реализации Functor и Applicative могут иметь смысл для деревьев HTML, но не для AST. Даже для HTML я не совсем понимаю назначение fmap и аппликативного <*>. Оба расширяют дерево, опуская узел и добавляя тип Empty. Я не совсем понимаю, какое естественное преобразование HTML-деревьев это представляет.

Я удивлен, что subTreeOf x (subTreeOf y) в определении аппликативного <*> на самом деле является правильным синтаксисом, или есть неявное >>?

Трансформации АСТ

Естественно применять преобразования к AST. PlaceHolder служит маленькой игрушкой для применения преобразования. Функция sub, имеющая здесь лишь частичную реализацию, должна заменить заполнитель "the_rest" комментарием. Однако необходимый sub (Append t t') s = Append (sub t s) (sub t' s) не компилируется, ожидаемый тип s(String, TreeM b), фактический тип — (String, TreeM a). С другой стороны, изменение типа на sub :: TreeM a -> (String, TreeM b) -> TreeM a нарушает определение sub p@(PlaceHolder n), и теперь я застрял.

На самом деле, разве это не sub именно то, чем должно быть fmap для AST?

Свободная монада?

Термин «свободная монада» регулярно всплывает, когда обсуждаются монады для AST. Но свободная монада опирается на Functor fmap для свободной конструкции, а показанное здесь fmap не подходит для AST. Как только правильное fmap определено, свободная монада должна сделать все остальное - возможно.

fmap

Кажется, что правильный fmap является ключом к успеху здесь, и правильный <*>, вероятно, станет более очевидным.

Случаи применения

Циклы можно записывать с помощью forM_, что является хорошим способом создания повторяющихся частей AST:

forM_ ["you", "get", "the", "idea"] $ \varName -> do
    Variable uint8 varName

Условные части могут использовать when, unless и т. д.

when hasCppDestructor $ do
    Comment "We need the destructor"
    Function NoReturnType "~SomeClass" $ do
        ...

Семантический анализ, например. обеспечение правильного порядка объявления также возможно, как было указано в первом ответе.

Визуальные подсказки. Еще мне нравится, что в конструкции, показанной выше, управляющие структуры, такие как if-then-else, forM_ и т. д., начинаются с нижнего регистра, тогда как строки AST начинаются с верхнего регистра.

Задний план

Несколько слов о том, к чему это, возможно, ведет: Идея состоит в том, чтобы использовать достаточно хороший встроенный DSL, который позволяет автоматически определять AST, который довольно абстрактно представляет, скажем, запутанный FSM, который необходимо реализовать на C, C++, Python, Java, Go, Rust, Javascript, что угодно... Функция render, подобная приведенной выше, затем сопоставляет достоверно правильный AST с целевым языком.

Обновления

  • Обратите внимание, что >> по умолчанию не *>, а m >> k = m >>= (\_ -> k)!

person mcmayer    schedule 07.01.2018    source источник
comment
Информацию о дешугаринге нотации do см. в отчете Haskell2010 здесь .   -  person Li-yao Xia    schedule 07.01.2018
comment
При представлении AST в виде монад конструктор Empty (как он здесь называется) соответствует дыре (содержащей временное значение), а (>>=) реализует подстановки в этих дырах (где новые выражения могут зависеть от временных значений, которые они заменяют). (>>) реализует замену константным выражением.   -  person Li-yao Xia    schedule 07.01.2018
comment
Append — это уловка, позволяющая подстановке вести себя как конкатенация. Я не уверен на 100%, что это удовлетворяет законам монад, но, похоже, это так (по модулю некоторых синтаксических тривиальностей, таких как тот факт, что Append следует рассматривать как моноидальную операцию).   -  person Li-yao Xia    schedule 07.01.2018
comment
>> по умолчанию не равно *>!   -  person dfeuer    schedule 07.01.2018
comment
@dfeuer m >> k = m >>= (\_ -> k). Хорошо понял.   -  person mcmayer    schedule 07.01.2018


Ответы (2)


Я не уверен, что весь этот подход является хорошей идеей (хотя на самом деле я несколько раз делал что-то подобное сам).

Обратите внимание, что такие монады, как Blaze.MarkupM, HaTeX.LaTeXM и т. д. на самом деле не являются монадами. На самом деле это всего лишь моноиды, которым нужен доступ к монадическим комбинаторам (в основном, чтобы злоупотреблять нотацией do, но она также допускает преобразование монад стека сверху, что может иметь некоторый смысл). То есть это не что иное, как специализированные Writer монады!
В данный момент вы действительно делаете то же самое; если это то, что вы намереваетесь, то, возможно, лучший способ сделать это было бы просто спроектировать ваш тип как Monoid Tree, затем посмотреть на структуру монады Writer Tree и, при желании, преобразовать ее в структуру данных TreeM. (HaTeX этого не делает, но сохраняет отдельные типы LaTeX и LaTeXM только с общим интерфейсом классов, что, возможно, является более чистым подходом, хотя и может быть неоптимальным для производительности.)

Результат будет очень похож на Blaze.MarkupM / структуру, которая у вас есть сейчас. Я мог бы обсудить ваши отдельные вопросы, но на самом деле на все они можно ответить, просто взглянув на то, насколько тип изоморфен монаде писателя.


На самом деле вам вообще не нужен экземпляр Monad для использования do как таковой:

Prelude> 2 * do 1 + 1
4

Поэтому, если вы просто хотите злоупотреблять do, чтобы избежать круглых скобок в древовидной структуре, но на самом деле не имеете разумного способа спрятать привязываемые переменные в своей структуре, рассмотрите возможность не писать какие-либо экземпляры монад. Этот экземпляр нужен только для блока do с несколькими строками, но если ни одна из этих строк не связывает какие-либо переменные, вы всегда можете просто заменить неявный >> явным <>, например

    Function Void "f" $ do
        Variable uint8 "local_y"
     <> Comment "etc. etc."

Единственная проблема заключается в том, что эти строки не могут включать оператор $, потому что он имеет более низкий приоритет, чем <>. Один отличный способ обойти это — заметить, что ($) = id, поэтому вы можете написать свой пример как

ast = do
    Variable uint8 "i"
 <> Function Void "f" `id`do
        Variable uint8 "local_y"
     <> Comment "etc. etc."

Вопрос о том, является ли это еще большим злоупотреблением синтаксисом, чем определение экземпляра «немного монады», остается спорным. IMO, если вы определяете такой экземпляр монады, вы должны сразу же сделать его преобразователем монады, как это делает HaTeX, потому что это также дает возможность разрешить включение IO действий в вашу сборку AST (например, для жестко включить внешние исходные файлы).


Все это говорит о том, что для вашего приложения может иметь смысл иметь экземпляр Monad, который не просто подслащенный моноид, но на самом деле связывает переменные полезным способом. . Эта функция не применима к blaze, но, безусловно, к языкам C++/Python/JavaScript, таким как AST, и она может быть весьма полезной, поскольку гарантирует, что переменные определены перед использованием прямо в синтаксисе Haskell. Вместо вашего примера вы бы написали

ast = do
    i <- variable uint8
    Function Void "f" $ do
        local_y <- variable uint8
        Comment "etc. etc."

Переменные под капотом на самом деле будут просто пронумерованными идентификаторами, выбранными в соответствии с переменной состояния.

Реализация будет примерно такой:

type VariableName = Int

data TreeS = Variable Type VariableName
           | Function Type VariableName TreeS
           | Comment String
           | PlaceHolder String
           | Append TreeS TreeS
           | Empty
instance Monoid where (<>) = Append

newtype TreeT m a
    = TreeT { runTreeM :: StateT VariableName (WriterT TreeS m) a }
    deriving (Functor, Applicative, Monad)

variable :: Type -> TreeT m VariableName
variable typ = TreeT $ do
   i <- get
   lift . tell $ Variable typ i
   put $ i+1
   return i
person leftaroundabout    schedule 07.01.2018
comment
Конечно, это не совсем монада. Используется только >>, и это явный намек на то, что с вычислительной точки зрения все, что нужно, — это моноид. Но чтобы получить нотацию «до», мне нужна монада, не так ли? Насколько я могу судить, нотация do достигает цели (то есть кратко определяет синтаксические деревья) с наименьшим синтаксическим шумом. - person mcmayer; 07.01.2018
comment
С правильным языковым расширением вы также можете использовать форму нотации do для стрелок: haskell.org/ стрелки/syntax.html - person Davislor; 07.01.2018
comment
@ Дэвислор, да, но какое отношение это имеет к этой теме? Тип OP на самом деле даже не функтор, не говоря уже о стрелке. - person leftaroundabout; 07.01.2018
comment
Это определенно функтор; только экземпляр немного схематичен. - person dfeuer; 07.01.2018
comment
@dfeuer конечно, но это функтор ради включения синтаксиса монады, а не потому, что он имеет какую-либо внутреннюю семантику функтора. Единственный способ, которым это имеет смысл, кроме злоупотребления синтаксисом, состоит в объединении этой тривиальной семантики монад с другими монадами, т. е. в настройке всего этого как монады transformer. - person leftaroundabout; 07.01.2018
comment
С монадой становятся доступными и другие приятные функции, такие как forM_ [1,2,3] (Comment . show) - person mcmayer; 07.01.2018
comment
@leftaroundabout Нарушение синтаксиса - о нет, нет, я определенно не хочу быть виноватым в этом ;-) Шутки в сторону. Я согласен со злоупотреблением синтаксисом, если это дает мне то, что я хочу, то есть AST и его читаемое определение без синтаксического шума, не несущего никакой информации. - person mcmayer; 07.01.2018
comment
s/forM_/flip foldMap/. — Конечно, я сам часто использую «немонадный do». Просто убедитесь, что вы не блокируете возможность использования синтаксиса для вещей, которые на самом деле являются монадическими, такими как другие преобразователи монад. - person leftaroundabout; 07.01.2018

Путь, который я выбрал с этой Append кодировкой AST, кажется тупиковым, поэтому я углубился в свободные монады. Вот результат:

бесплатная монада — хороший подходит для такого типа задач. Свободные монады позволяют отделить «логику» программы от ее эффектов. AST попадают в эту модель. В этом примере «логика» - это AST, и эффект просто хорош.

В более общем смысле «эффект» может означать анализ, тестирование (например, пробные прогоны), выполнение корректур, красивую печать, сжатие и, конечно же, фактическое выполнение.

О бесплатных монадах написано много, вот несколько полезных ресурсов для начала:

Теперь, используя Control.Monad.Free, решение будет выглядеть так:

{-# LANGUAGE DeriveFunctor #-}
module Main where

import           Control.Monad.Free
import           Data.Monoid        ((<>))
import           Data.String.Utils  (rstrip)

type NumberOfBits = Word
type VariableName = String

data Type = UInt NumberOfBits
        | Int NumberOfBits
        | Void
        deriving Eq

uint8 = UInt 8
int8 = Int 8

instance Show Type where
    show (UInt w) = "uint" <> show w
    show (Int w)  = "int" <> show w
    show Void     = "void"

data AST n = Variable Type VariableName n                -- variable declaration
            | Function Type VariableName (Free AST ()) n -- function declaration
            | Comment String n                           -- a comment
            | PlaceHolder String n                       -- a placeholder with @name holds holds more code
            | End                                        
            deriving (Eq, Show, Functor)

end :: Free AST ()
end = liftF End -- is exactly Pure ()

variable :: Type -> VariableName -> Free AST ()
variable y n = liftF (Variable y n ())

function :: Type -> VariableName -> Free AST () -> Free AST ()
function t n p = liftF (Function t n p ())

placeHolder :: String -> Free AST ()
placeHolder n = liftF (PlaceHolder n ())

comment :: String -> Free AST ()
comment c = liftF (Comment c ())

indent :: String -> String
indent s = rstrip $ unlines $ map ("    "<>) (lines s)

render :: Free AST r -> String
render (Free (Variable y n next))   = "Variable " <> show y <> " " <> show n <> "\n" <> render next
render (Free (Function t n f next)) = "Function \"" <> n <> "\" returning " <> show t <> ":\n"
                                    <> indent (render f) <> "\n" <> render next
render (Free (Comment c next))      = "//  " <> c <> "\n" <> render next
render (Free (PlaceHolder s next))  = "PlaceHolder \"" <> s <> "\"\n" <> render next
render (Free End)                   = "end"
render (Pure r)                     = "return\n"

code :: Free AST ()
code = do
    placeHolder "includefiles"
    variable uint8 "i"
    variable int8 "j"
    function Void "f" $ do
        comment "This is a function!"
        variable (Int 8) "local_i"

sub :: AST (Free AST b) -> Free AST b
sub (Variable t n next) = do
    variable t n
    next
sub (Function t n f next) = do
    function t n f
    next
sub (Comment c next) = do
    comment c
    next
sub (PlaceHolder s next) = do
    comment "placeholder"
    next

main :: IO ()
main = do
    putStrLn $ render code
    putStrLn "-- Apply subst\n"
    putStrLn $ render (iterM sub code)

Не все это нужно так подробно описывать. Некоторые шаблоны можно удалить с помощью Control.Monad.Free.TH.

Control.Monad.Free в некотором роде является канонической реализацией, но цепная структура данных означает квадратичную сложность некоторых операций. Это было рассмотрено самим автором, Эдом Кметтом, в Control.Monad.Free.Church, где используется другая кодировка. См. тест бесплатных монад, где можно найти тесты и указатели на другие реализации бесплатных монад.

Выходя за рамки свободных монад, сосвободные монады формализуют интерпретатор и его связь с «логикой». См., например, "Free для DSL, cofree для переводчиков" Дэвида Лэйнга.

person mcmayer    schedule 09.01.2018