Как можно использовать квазикотацию Хаскелла для замены токенов на уровне Haskell?

Квазиквитация, описанная в haskellwiki, показана в основном как полезный инструмент для встраивания других языков внутри Haskell без использования котировок строк.

Вопрос: Для самого Haskell, как легко было бы поместить существующий код Haskell через квазикватер с целью просто заменить токены и передать результат на ghc? Возможно, Шаблон Haskell здесь ключевой?

Я искал примеры кода и не нашел. Некоторые EDSL могут извлечь выгоду из этой способности, уменьшив размер своих операторов (например, "a".b. → . C "на" [myedsl | a | b → c] ").

Ответы

Ответ 1

Вы можете построить квазициклы, которые манипулируют кодом Haskell, например, используя пакет haskell-src-meta. Он анализирует действительный код Haskell в AST, который вы затем можете изменить.

В этом случае самый простой способ изменить AST - это использовать Data.Generics, чтобы применить общее преобразование ко всему АСТ, которое заменяет операторов другими операторами.

Начнем с построения функции преобразования для генерических выражений Haskell. Тип данных, представляющий выражение, Exp в пакете template-haskell.

Например, чтобы преобразовать оператор >> в .>>., мы использовали бы функцию типа

import Language.Haskell.TH (Exp(..), mkName)

replaceOp :: Exp -> Exp
replaceOp (VarE n) | n == mkName ">>" = VarE (mkName ".>>.")
replaceOp e = e

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

Теперь, чтобы пройти весь АСТ и заменить все вхождения >>, мы будем использовать функции everywhere и mkT от Data.Generic.

import Data.Generics (everywhere, mkT)

replaceEveryOp :: Exp -> Exp
replaceEveryOp = everywhere (mkT replaceOp) 

Чтобы сделать несколько замен, мы можем изменить функцию так, чтобы она заменила список ассоциаций любого оператора.

type Replacements = [(String, String)]

replaceOps :: Replacements -> Exp -> Exp
replaceOps reps = everywhere (mkT f) where
    f [email protected](VarE n) = case rep of
        Just n' -> VarE (mkName n')
        _ -> e
        where rep = lookup (show n) reps
    f e = e

И, кстати, это хороший пример функции, которая гораздо приятнее писать, используя расширение шаблонов просмотра.

{-# LANGUAGE ViewPatterns #-}

replaceOps :: Replacements -> Exp -> Exp
replaceOps reps = everywhere (mkT f) where
    f (VarE (replace -> Just n')) = VarE (mkName n')
    f e = e

    replace n = lookup (show n) reps

Теперь все, что нам осталось сделать, - это построить квазициклер "myedsl".

{-# LANGUAGE ViewPatterns #-}

import Data.Generics (everywhere, mkT)
import Language.Haskell.Meta.Parse (parseExp)
import Language.Haskell.TH (Exp(..), mkName, ExpQ)
import Language.Haskell.TH.Quote (QuasiQuoter(..))

type Replacements = [(String, String)]

replacements :: Replacements
replacements =
    [ ("||", ".|.")
    , (">>", ".>>.")
    ]

myedls = QuasiQuoter
    { quoteExp  = replaceOpsQ
    , quotePat  = undefined
    , quoteType = undefined
    , quoteDec  = undefined
    }

replaceOpsQ :: String -> ExpQ
replaceOpsQ s = case parseExp s of
    Right e -> return $ replaceOps replacements e
    Left err -> fail err

replaceOps :: Replacements -> Exp -> Exp
replaceOps reps = everywhere (mkT f) where
    f (VarE (replace -> Just n')) = VarE (mkName n')
    f e = e

    replace n = lookup (show n) reps

Если вы сохраните это в своем собственном модуле (например, MyEDSL.hs), вы можете импортировать его и использовать квазициклер.

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}

import MyEDSL

foo = [myedsl| a || b >> c |]

Обратите внимание, что я использовал || вместо |, потому что последний не является допустимым оператором в Haskell (так как это синтаксический элемент, используемый для защиты шаблонов).