Расширение типа данных в Haskell
Хаскелл здесь новичок.
Я написал оценщик для минимального ассемблерного языка.
теперь я хочу расширить этот язык, чтобы поддержать некоторый синтаксический сахар, который я затем скомпилирую обратно, чтобы использовать только примитивные операторы. Ideia заключается в том, что я не хочу снова прикасаться к модулю оценщика.
в ОО способ делать вещи, я думаю, можно расширения исходный модуль для поддержки синтаксических операторов сахара, предоставление здесь правил перевода.
кроме этого, я могу думать только о переписывании конструкторов типов данных в обоих модулях, чтобы они не сталкивались с именами и исходили оттуда, как будто они были полными разными вещами, но это подразумевает некоторую избыточность, поскольку мне пришлось бы повторять (только с другими именами) общие операторы. Опять же, я думаю, что ключевое слово здесь расширения.
есть ли функциональный способ выполнить это?
Спасибо, что нашли время прочитать этот вопрос.
4 ответов
эта проблема была названа" проблемой выражения " Филом Уодлером, по его словам:
цель состоит в том, чтобы определить тип данных по случаям, где можно добавлять новые случаи в тип данных и новые функции по типу данных, без перекомпиляции существующего кода и при сохранении статическая типизация.
одним из решений расширенного типа данных с помощью классов типов.
в качестве примера предположим, что у нас есть простой язык арифметика:
data Expr = Add Expr Expr | Mult Expr Expr | Const Int
run (Const x) = x
run (Add exp1 exp2) = run exp1 + run exp2
run (Mult exp1 exp2) = run exp1 * run exp2
например
ghci> run (Add (Mult (Const 1) (Const 3)) (Const 2))
5
если мы хотим реализовать его расширяемым способом, мы должны переключиться на классы типов:
class Expr a where
run :: a -> Int
data Const = Const Int
instance Expr Const where
run (Const x) = x
data Add a b = Add a b
instance (Expr a,Expr b) => Expr (Add a b) where
run (Add expr1 expr2) = run expr1 + run expr2
data Mult a b = Mult a b
instance (Expr a, Expr b) => Expr (Mult a b) where
run (Mult expr1 expr2) = run expr1 * run expr2
теперь давайте расширим язык, добавляя вычитания:
data Sub a b = Sub a b
instance (Expr a, Expr b) => Expr (Sub a b) where
run (Sub expr1 expr2) = run expr1 - run expr2
например
ghci> run (Add (Sub (Const 1) (Const 4)) (Const 2))
-1
для получения дополнительной информации об этом подходе и в целом о проблеме выражения проверьте видео Ральфа Леммеля 1 и 2 на 9-м канале.
однако, как замечено в комментарии, это решение изменяет семантику. Например, списки выражений больше не являются законными:
[Add (Const 1) (Const 5), Const 6] -- does not typecheck
более общее решение с использованием копродуктов сигнатур типов представлено в функциональной жемчужине "типы данных a la carte". См. также Уадлер по комментарий на бумаге.
Вы можете сделать что-то немного больше ООП-как с помощью экзистенциальных типов:
-- We need to enable the ExistentialQuantification extension.
{-# LANGUAGE ExistentialQuantification #-}
-- I want to use const as a term in the language, so let's hide Prelude.const.
import Prelude hiding (const)
-- First we need a type class to represent an expression we can evaluate
class Eval a where
eval :: a -> Int
-- Then we create an existential type that represents every member of Eval
data Exp = forall t. Eval t => Exp t
-- We want to be able to evaluate all expressions, so make Exp a member of Eval.
-- Since the Exp type is just a wrapper around "any value that can be evaluated,"
-- we simply unwrap that value and call eval on it.
instance Eval Exp where
eval (Exp e) = eval e
-- Then we define our base language; constants, addition and multiplication.
data BaseExp = Const Int | Add Exp Exp | Mul Exp Exp
-- We make sure we can evaluate the language by making it a member of Eval.
instance Eval BaseExp where
eval (Const n) = n
eval (Add a b) = eval a + eval b
eval (Mul a b) = eval a * eval b
-- In order to avoid having to clutter our expressions with Exp everywhere,
-- let's define a few smart constructors.
add x y = Exp $ Add x y
mul x y = Exp $ Mul x y
const = Exp . Const
-- However, now we want subtraction too, so we create another type for those
-- expressions.
data SubExp = Sub Exp Exp
-- Then we make sure that we know how to evaluate subtraction.
instance Eval SubExp where
eval (Sub a b) = eval a - eval b
-- Finally, create a smart constructor for sub too.
sub x y = Exp $ Sub x y
делая это, мы фактически получаем один расширяемый тип, чтобы вы могли, например, смешивать расширенные и базовые значения в списке:
> map eval [sub (const 10) (const 3), add (const 1) (const 1)]
[7, 2]
однако, поскольку единственное, что мы теперь можем знать о значениях Exp, - это то, что они каким-то образом являются членами Eval, мы не можем сопоставлять шаблоны или делать что-либо еще, что не указано в классе type. В терминах ООП подумайте о Exp значение exp как объект, реализующий интерфейс Eval. Если у вас есть объект типа ISomethingThatCanBeEvaluated, очевидно, вы не можете безопасно поместить его во что-то более конкретное; то же самое относится к Exp.
синтаксический сахар обычно обрабатывается синтаксическим анализатором; вы бы расширили (не в смысле наследования OO) синтаксический анализатор, чтобы обнаружить новые конструкции и перевести их в структуры, которые может обрабатывать ваш оценщик.
(более простой) вариант-добавить тип к вашему AST, чтобы отличить Core от Extended:
data Core = Core
data Extended = Extended
data Expr t
= Add (Expr t) (Expr t)
| Mult (Expr t) (Expr t)
| Const Int
| Sugar t (Expr t) (Expr t)
выражение является либо Core, либо Extended: компилятор гарантирует, что он содержит только под-выражения того же типа.
сигнатуры функций в исходном модуле должны использовать Expr Core
(вместо Expr
)
функция Desugar будет иметь следующую сигнатуру типа:
Desugar :: Expr Extended -> Expr Core
вы также можете быть заинтересованы в более сложный подход, описанный в статьедеревьев, которые растут'.