Проверка правильности программы с использованием типов phantom в Haskell
Предположим, что я работаю с кодом стекового компьютера, который может выполнять некоторые простые операции (push, add, mul, dup, swap, pop, convert types) на ints и double.
Теперь программа, которую я пишу, берет описание на каком-то другом языке и переводит его в код для этой стековой машины. Мне также нужно вычислить максимальный размер стека.
Я подозреваю, что можно использовать проверку типа Haskell для устранения некоторых ошибок, например:
- выскакивание из пустого стека
умножение удваивается с использованием умножения int
Я думал, что могу объявить, например:
dup :: Stack (a :%: b) -> Stack (a :%: b :%: b)
int2double :: Stack (a :%: SInt) -> Stack (a :%: SDouble)
и т.д. Но тогда я не знаю, как сгенерировать код и рассчитать размер стека.
Возможно ли это сделать так? И будет ли это просто/удобно/стоит?
См. Крис Окасаки "Методы внедрения Postfix Languages в Haskell": http://www.eecs.usma.edu/webs/people/okasaki/pubs.html#hw02
Кроме того, это:
{-# LANGUAGE TypeOperators #-}
module Stacks where
data a :* b = a :* b deriving Show
data NilStack = NilStack deriving Show
infixr 1 :*
class Stack a where
stackSize :: a -> Int
instance Stack b => Stack (a :* b) where
stackSize (_ :* x) = 1 + stackSize x
instance Stack NilStack where
stackSize _ = 0
push :: Stack b => a -> b -> a :* b
push = (:*)
pop :: Stack b => a :* b -> (a,b)
pop (x :* y) = (x,y)
dup :: Stack b => a :* b -> a :* a :* b
dup (x :* y) = x :* x :* y
liftBiOp :: Stack rest => (a -> b -> c) -> a :* b :* rest -> c :* rest
liftBiOp f (x :* y :* rest) = push (f x y) rest
add :: (Stack rest, Num a) => a :* a :* rest -> a :* rest
add = liftBiOp (+)
{-
demo:
*Stacks> stackSize $ dup (1 :* NilStack)
2
*Stacks> add $ dup (1 :* NilStack)
2 :* NilStack
-}
Поскольку ваш стек различается по типу, вы не можете упаковать его в обычную государственную монаду (хотя вы можете упаковать ее в параметризованную монаду, но это другая история), но кроме этого, это должно быть простым, приятным, и статически проверяется.
Это может вас заинтересовать:
https://github.com/gergoerdi/arrow-stack-compiler/blob/master/StackCompiler.hs
Это простой ассемблер, который поддерживает размер стека в своем типе. Например. следующие две сигнатуры указывают, что binOp
, заданный код, который работает в двух регистрах и оставляет размер стека as-is, создает код, который выдает два аргумента из стека и толкает результат. compileExpr
использует binOp
и другие конструкторы для создания кода, который оценивает выражение и толкает его поверх стека.
binOp :: (Register -> Register -> Machine n n) -> Machine (S (S n)) (S n)
compileExpr :: Expr -> Machine n (S n)
Обратите внимание, что это всего лишь доказательство того, что я просто загрузил его в GitHub только для того, чтобы показать вам, поэтому не ожидайте найти в нем ничего хорошего.
Просто и удобно? Я не уверен.
Я бы начал с описания проблемы. Задача состоит в том, чтобы перейти от неофициального к спецификации, которая ближе к тому, что у нас есть в Haskell (типы).
Здесь есть две проблемы: принудительное включение инвариантов на основе ввода на языке ввода (арифметические выражения?) и обеспечение того, чтобы выражение исходного языка, скомпилированное в программу стекового компьютера, фактически выполняло то, что мы хотим.
Первое можно легко решить с помощью GADT: вам просто нужно индексировать выражения по их типам (например, Expr a для выражений, которые оценивают значение типа a).
Второй, не так уверен. Разумеется, вы можете, конечно, индексировать списки по типам натуралов (например, с помощью GADT). Типы определенных функций в списках (например, голова и хвост) становятся настолько точными, что мы можем сделать их тотальными. Является ли стек вашей стековой машины однородным (то есть содержит только целые числа или только удваивает или...)?
Другие свойства могут быть закодированы (и принудительно), но для этого маршрута могут потребоваться значительные усилия со стороны программиста.
Я думаю, что это должно быть возможно без проблем, но вы сталкиваетесь с проблемами, когда пытаетесь сделать что-то в цикле. Чем вам нужны такие забавные вещи, как натуральные числа типа. Рассмотрим такую функцию:
popN :: Int -> Stack (?????)
Но если вам не нужны такие вещи, не стесняйтесь делать все, что хотите. BTW, петли работают только в том случае, если количество элементов одинаковое до и после, иначе он не будет компилироваться. (A-la бесконечный тип).
Вы можете попытаться исправить это, используя классы типа, но я предполагаю, что вы пытаетесь сделать это лучше использовать язык с зависимыми типами.
- Вопросы
- Correctness
- Проверка правильности программы с использованием типов phantom в Haskell