Расширение типа данных в Haskell

Haskell новичок здесь.

Я написал оценщика для минимального сборочного языка.

Теперь я хочу расширить этот язык, чтобы поддерживать какой-то синтаксический сахар, который затем я буду компилировать, чтобы использовать только примитивные операторы. Идея заключается в том, что я больше не хочу касаться модуля оценки.

В OO-способе делать вещи, я думаю, можно было расширить исходный модуль, чтобы поддерживать синтаксические сахара-операторы, предоставляя здесь правила трансляции.

Кроме того, я могу только подумать о том, чтобы переписать конструкторы типов данных в обоих модулях, чтобы они не назовели имя и не перешли оттуда, как если бы они были совершенно разными, но это означает некоторую избыточность, поскольку я бы должны повторять (только с другими именами) общие операторы. Опять же, я думаю, что ключевое слово здесь expand.

Есть ли функциональный способ достижения этого?

Спасибо, что нашли время, чтобы прочитать этот вопрос.

Ответ 1

Эта проблема была названа "проблемой выражения" Фила Вадлера, по его словам:

Цель состоит в том, чтобы определить тип данных по случаям, где можно добавить новые типы данных к типу данных и новым функциям над типом данных без перекомпиляции существующего кода и при сохранении безопасность статического типа.

Одно из решений иметь расширяемый тип данных - использовать классы типов.

В качестве примера допустим, что у нас есть простой язык для арифметики:

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

Для получения дополнительной информации об этом подходе и вообще о проблеме с выражением отметьте видео Ralf Laemmel 1 и 2 на канале 9.

Однако, как замечено в комментариях, это решение изменяет семантику. Например, списки выражений более не являются законными:

[Add (Const 1) (Const 5), Const 6] -- does not typecheck

В функциональном жемчуге "Типы данных a la carte" представлено более общее решение с использованием копедуктов сигнатур типа.. См. Также Wadler комментарий на бумаге.

Ответ 2

Вы можете сделать что-то более похожее на ООП, используя экзистенциальные типы:

-- We need to enable the ExistentialQuantification extension.
{-# LANGUAGE ExistentialQuantification #-}

-- I want to use const as a term in the language, so let 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 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, мы не можем сопоставлять шаблон или делать что-либо еще, что не указано в классе типов. В терминах ООП, подумайте о значении Exp exp, как об объекте, реализующем интерфейс Eval. Если у вас есть объект типа ISomethingThatCanBeEvaluated, очевидно, что вы не можете смело использовать его в чем-то более конкретном; то же самое относится к Exp.

Ответ 3

Синтаксический сахар обычно обрабатывается парсером; вы должны расширить (не в смысле OO-наследования) парсер для обнаружения новых конструкций и перевести их в структуры, которые может обработать ваш оценщик.