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