Расширение типа данных в 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

вы также можете быть заинтересованы в более сложный подход, описанный в статьедеревьев, которые растут'.