Abstrakcja i dopasowanie do wzorców w Haskellu
Po hermetyzacji i abstrakcji RAMu dla interpretera HelMA pora na stos. Stos, zwłaszcza stos arytmetyczny, jest strukturą używaną w wielu interpreterach jezyków ezoterycznych. Więc warto wydzielić tą abstrakcję do osobnego modułu.
Żeby zaimplementować stos będziemy potrzebować dopasowania do wzorców (ang. pattern matching), niestety abstrakcje i dopasowanie do wzorców są to pojęcia kłócące się.
Ponieważ dopasowanie do wzorców działa na implementacji, Trzeba opakować dopasowanie do wzorców w abstrakcje.
Abstrakcja i klasy typów
Spójrzmy na moduł HelVM.HelCam.Common.Memories.Stack
, dla czytelności podzielony na pięć listingów.
Najpierw deklaracje i importy:
{-# Language AllowAmbiguousTypes #-}
{-# Language FlexibleInstances #-}
{-# Language MultiParamTypeClasses #-}
module HelVM.HelCam.Common.Memories.Stack (
Index,
Stack,
select,
HelVM.HelCam.Common.Memories.Stack.empty,
HelVM.HelCam.Common.Memories.Stack.lookup,
HelVM.HelCam.Common.Memories.Stack.splitAt',
HelVM.HelCam.Common.Memories.Stack.drop',
push1,
pop1,
push2,
pop2
) where
import Data.Sequence as Seq
type Index = Int
Jak się później okaże, eksporty zawierają jeden typ, jedną klasę typów (ang. Type Class) i 9 funkcji, w tym jedna funkcja generyczna i osiem metod (uparcie nazywam tak funkcje w klasach typów).
Następnie kod, który normalnie znalazłby się w klasie bazowej:
select :: Stack s m => Index -> m -> s
select i stack = check $ HelVM.HelCam.Common.Memories.Stack.lookup i stack where
check (Just symbol) = symbol
check Nothing = error $ "Empty stack " <> show stack <> " index " <> show i
Definiujemy jedną funkcję generyczną. Czemu tylko jedną? O tym później.
Abstrakcja oparta na klasie typu
Nasz stos będzie potrzebować 8 podstawowych metod.
Podobnie jak dla klasy typów RAM
potrzebujemy klasy typów dla dwóch parametrów, symbolu s
i pamieci m
:
class (Semigroup m, Show m) => Stack s m where
empty :: m
lookup :: Index -> m -> Maybe s
splitAt' :: s -> Index -> m -> (m, m)
drop' :: s -> Index -> m -> m
push1 :: s -> m -> m
pop1 :: m -> (s, m)
push2 :: s -> s -> m -> m
pop2 :: m -> (s, s, m)
Są tu dwie brzydkie metody splitAt'
i drop'
. Wynika to z tego, że w każdej sygnaturze muszą być użyte oba parametry generyczne. Niestety nie umiałem tego napisać lepiej.
Implementacja oparta na liście
Najpierw prostsza implementacja dla listy:
instance Show s => Stack s [s] where
empty = []
lookup i stack = stack !!? i
splitAt' _ i stack = Prelude.splitAt i stack
drop' _ i stack = Prelude.drop i stack
push1 symbol stack = symbol: stack
pop1 (symbol : stack) = (symbol, stack)
pop1 stack = error $ "Empty stack " <> show stack
push2 symbol symbol' stack = symbol: symbol': stack
pop2 (symbol : symbol' : stack) = (symbol, symbol', stack)
pop2 stack = error $ "Empty stack " <> show stack
Mamy tu klasyczne dopasowanie do wzorców dla list. Nic nadzwyczajnego.
Implementacja oparta na sekwencji
Implementacja dla sekwencji:
instance Show s => Stack s (Seq s) where
empty = Seq.fromList []
lookup i stack = Seq.lookup i stack
splitAt' _ i stack = Seq.splitAt i stack
drop' _ i stack = Seq.drop i stack
push1 symbol stack = symbol <| stack
pop1 (symbol :<| stack) = (symbol, stack)
pop1 stack = error $ "Empty stack " <> show stack
push2 symbol symbol' stack = symbol <| symbol' <| stack
pop2 (symbol :<| symbol' :<| stack) = (symbol, symbol', stack)
pop2 stack = error $ "Empty stack " <> show stack
Mamy tu dwa nowe operatory :<|
dla dopasowania do wzorców oraz <|
dla dołączania do sekwencji. Jeśli operowalibyśmy na drugim końcu sekwencji należałoby użyć :|>
i |>
.
Ograniczenia
Tutaj pojawia się mały zgrzyt. Niestety nie potrafię zdefinioć niektórych funkcji generycznych dla parametru generycznego Stack s m
. Na razie musimy zadowolić się mniej genrycznymi wersjami dla parametru Stack Symbol m
. Funkcje te są zdefiniowane w modułach HelVM.HelCam.Machines.ETA.StackOfSymbols
oraz HelVM.HelCam.Machines.WhiteSpace.StackOfSymbols
.
Funkcje pomocnicze dla interpretera eso języka ETA:
{-# Language FlexibleContexts #-}
module HelVM.HelCam.Machines.ETA.StackOfSymbols where
import HelVM.HelCam.Machines.ETA.EvaluatorUtil
import HelVM.HelCam.Common.Memories.Stack
-- Arithmetic
divMod :: Stack Symbol m => m -> m
divMod stack = push2 (symbol' `mod` symbol ::Symbol) (symbol' `div` symbol ::Symbol) stack'
where (symbol, symbol', stack') = pop2 stack
sub :: Stack Symbol m => m -> m
sub stack = push1 (symbol' - symbol ::Symbol) stack'
where (symbol, symbol', stack') = pop2 stack
-- Stack instructions
halibut :: Stack Symbol m => m -> m
halibut stack
| i <= 0 = copy (negate i) stack'
| otherwise = move (0 ::Symbol) i stack'
where (i, stack') = pop1 stack
move :: Stack Symbol m => Symbol -> Index -> m -> m
move symbol i stack = tops <> middles <> bottoms where
(middles, stack') = splitAt' symbol i stack
(tops, bottoms) = splitAt' symbol 1 stack'
copy :: Stack Symbol m => Index -> m -> m
copy i stack = push1 (select i stack ::Symbol) stack
Funkcje pomocnicze dla interpretera eso języka WhiteSpace:
{-# Language AllowAmbiguousTypes #-}
{-# Language FlexibleContexts #-}
{-# Language FlexibleInstances #-}
{-# Language MultiParamTypeClasses #-}
module HelVM.HelCam.Machines.WhiteSpace.StackOfSymbols where
import HelVM.HelCam.Machines.WhiteSpace.EvaluatorUtil
import HelVM.HelCam.Machines.WhiteSpace.Instruction
import HelVM.HelCam.Common.Memories.Stack
-- Arithmetic
binaryOp :: Stack Symbol m => BinaryOperator -> m -> m
binaryOp op stack = push1 (doBinary op symbol symbol' ::Symbol) stack' where (symbol, symbol', stack') = pop2 stack
-- Stack instructions
swap :: Stack Symbol m => m -> m
swap stack = push2 (symbol'::Symbol) symbol stack' where (symbol, symbol', stack') = pop2 stack
discard :: Stack Symbol m => m -> m
discard = drop' (0::Symbol) 1
slide :: Stack Symbol m => Index -> m -> m
slide i stack = push1 (symbol::Symbol) (drop' (0::Symbol) i stack') where (symbol, stack') = pop1 stack
dup :: Stack Symbol m => m -> m
dup = copy 0
copy :: Stack Symbol m => Index -> m -> m
copy i stack = push1 (select i stack ::Symbol) stack
Jak widać funkcja copy :: Stack Symbol m => Index -> m -> m
jest w obu modułach, jednak nie potrafię jej w prosty sposób uogólnić.
Przykład użycia
Jako przykład użycia fragment modułu HelVM.HelCam.Machines.ETA.Evaluator
:
class Evaluator r where
next :: Stack Symbol m => InstructionUnit -> m -> r
next iu s = doInstruction t iu' s where (t, iu') = nextIU iu
doInstruction :: Stack Symbol m => Maybe Token -> InstructionUnit -> m -> r
-- IO instructions
doInstruction (Just O) iu s = doOutputChar iu s
doInstruction (Just I) iu s = doInputChar iu s
-- Stack instructions
doInstruction (Just N) iu s = next iu' (push1 (symbol::Symbol) s) where (symbol, iu') = parseNumber iu
doInstruction (Just H) iu s = next iu $ halibut s
-- Arithmetic
doInstruction (Just S) iu s = next iu $ sub s
doInstruction (Just E) iu s = next iu $ Stack.divMod s
-- Control
doInstruction (Just R) iu s = next iu s
doInstruction (Just A) iu@(IU il ic) s = next iu (push1 (nextLabel il ic) s)
doInstruction (Just T) iu@(IU il _ ) s = transfer $ pop2 s where
transfer (_, 0, s') = next iu s'
transfer (0, _, _ ) = doEnd
transfer (l, _, s') = next (IU il $ findAddress il l) s'
doInstruction Nothing _ _ = doEnd
----
doEnd :: r
doOutputChar :: Stack Symbol m => InstructionUnit -> m -> r
doInputChar :: Stack Symbol m => InstructionUnit -> m -> r
Cel został osiągnięty. Struktura stosu została schowana za abstrakcją klasy typu Stack
. Teraz można swobodnie wymieniać implementację między listą a sekwencją.
Podsumowanie
Mimo że cel został osiągnięty, kod jednak nie wyszedł tak dobry jak chciałem. Może to pora na użycie Rodzin typów?
Kod interpretera HelMA po zmianach znajduje się na githubie.
Zostaw komentarz