Я новичок в haskell и мог бы воспользоваться некоторыми отзывами о моем коде и решениях, которые мне нужно было принять. В моем предыдущем проекте я создал парсер JSON, но в значительной степени полагался на рекомендации университетского курса.
Этот проект представляет собой интерпретатор мозгов, для которого я не использовал ничего, кроме статьи в вики о мозговом учении (т. Е. Без чужого дизайна или кода, кроме whileM). В некотором смысле это открыло мне глаза, потому что я, наконец, использовал и разработал монадические операции сам. Но я уверен, что есть еще много глаз, на которые я слеп, поэтому, пожалуйста, укажите, где я ошибаюсь или пропускаю более элегантные решения.
В этой реализации отсутствует какой-либо ввод-вывод, потому что я хотел создать простую базовую реализацию вычислений с отслеживанием состояния. Цель состоит в том, чтобы впоследствии использовать StateT вместо State и объединить его с IO, чтобы я правильно научился использовать преобразователи монад.
В частности, я не уверен в следующих моментах:
- Модель данных
Я сделал ленту типа с указателем, который можно перемещать и читать / писать. Я использую этот тип как для ленты данных, так и для самой программы brainfuck, ленты с инструкциями. Функциональные возможности двух лент совпадают, но не полностью. Например, операция «увеличить байт в указателе» нужна только для ленты данных, а «искать следующую совпадающую скобку» — только для ленты с инструкциями (строки 1–100).
Возникает вопрос: можно ли создать один тип, для которого я реализую ВСЕ функции, или мне лучше разделить его на два типа и продублировать код для общих инструкций (большинство из них являются общими)? Есть ли идиоматический способ «унаследовать» типы от других типов?
- Превращение его в государственную монаду
В строках 149–188 я реализую восемь мозговых операций как операции преобразования состояния. Хотя они делают совершенно разные вещи, я считать Мне нужно, чтобы они были одного типа из-за функции chooseAction (строка 232): между различными шагами программы brainfuck следующая инструкция должна считываться с ленты инструкций и превращаться в фактический исполняемый преобразователь состояния. Поскольку эта функция chooseAction должна возвращать значения одного и того же типа, все мои монадические операции должны иметь один и тот же тип … Верно? Как этого лучше всего достичь?
В моем случае я решил создать тип Effect, который в настоящее время имеет два конструктора, но который я могу легко расширить, создав преобразователи состояний с новыми эффектами. Большинство преобразователей состояний (>, <, +, - и т.д.) не имеют возвращаемого значения, поэтому они возвращают None :: Effect. Но преобразователь состояния sOutp, который соответствует '.', Выводит байт. Именно здесь в конечном итоге появится IO, но на данный момент я просто хочу, чтобы этот байт был захвачен типом Effect и возвращался монадической функции, собирающей эффект, whileM :: Monad => m Bool -> ma — > м [a].
Помимо использования здесь монады ввода-вывода и превращения State в StateT, есть ли лучший способ сделать то, что я хочу здесь? И поэтому все мои 8 монадических операций с мозгами должны быть одного и того же типа, даже если они делают такие разные вещи?
3)
Строки 150–188 кажутся немного многословными, потому что мне кажется, что я повторяю одно и то же снова и снова. Мне здесь не хватает элегантного способа или это просто утомительная часть реализации?
Спасибо за любой вклад!
Номера строк см. В этом файле: https://github.com/el-micha/bf/blob/master/bf.hs
Тот же код воспроизводится ниже. Чтобы запустить, попробуйте:
runTM testtm
а также runTM testtm2
import Control.Monad
import Data.Char (ord, chr)
import Data.Word (Word8)
-- Implementation without IO. Basecase for future IO extension with MonadTransformers.
-- ============================================================================
-- Data types for data and instruction tapes and their operations
-- ============================================================================
-- keep track of tape length with tuple (pointer position, tape length so far)
type Bound = (Int, Int)
leftB :: Bound -> Bound
leftB (x, y) = (x-1, y)
rightB :: Bound -> Bound
rightB (x, y) = (x+1, max y (x+1))
-- a type for both the data tape and the instruction tape.
-- pointer is head of first list. left of pointer is field1[1], right of pointer is field2[0]
-- [1 2 3 4 5 6]
-- ^
-- corresponds to Tape [3, 2, 1] [4, 5, 6] (2, 5)
type Byte = Word8
data Tape = Tape [Byte] [Byte] Bound
zeroes = [0 | _ <- [1..]]
emptyTape :: Tape
emptyTape = Tape [0] zeroes ((0, 0) :: Bound)
initTape :: [Byte] -> Tape
initTape (x:xs) = Tape [x] (xs++zeroes) ((0, length xs) :: Bound)
-- shift pointer by one
left :: Tape -> Tape
left (Tape [] _ _) = error "Cannot shift tape to left: Is at origin. Bad initialization."
left (Tape [x] _ _) = error "Cannot shift tape to left: Is at leftmost cell."
left (Tape (x:xs) ys b) = Tape xs (x:ys) (leftB b)
right :: Tape -> Tape
right (Tape xs (y:ys) b) = Tape (y:xs) ys (rightB b)
ptrPos (Tape xs ys b) = fst b
tapeLength (Tape xs ys b) = snd b
-- read from pointer position
readTape :: Tape -> Byte
readTape (Tape (ptr:xs) ys b) = ptr
-- is byte at ptr 0?
isZero :: Tape -> Bool
isZero = (==0) . readTape
isChar :: Char -> Tape -> Bool
isChar c = (==c) . chr . fromEnum . readTape
-- write to pointer position
writeTape :: Byte -> Tape -> Tape
writeTape n (Tape (ptr:xs) ys b) = Tape (n:xs) ys b
--increment, decrement the byte at ptr
increment :: Tape -> Tape
increment (Tape (ptr:xs) y b) = Tape ((ptr+1):xs) y b
decrement :: Tape -> Tape
decrement (Tape (ptr:xs) y b) = Tape ((ptr-1):xs) y b
inc = increment
dec = decrement
instance Show Tape where
show (Tape (ptr:xs) ys b) = show (reverse xs) ++ " " ++ show ptr ++ " " ++ show (take (snd b - fst b) ys)
-- for the instruction tape
show' (Tape (x:xx) yy b) = reverse xs ++ " " ++ [ptr] ++ " " ++ take (snd b - fst b) ys
where ptr = f x
xs = map f xx
ys = map f yy
f = chr . fromEnum
-- ============================================================================
-- Instruction Tape: reuse tape but add some functions
-- find matching ], assuming program is syntactically correct, i.e., there IS a matching ] AND the tape points to a [
-- same with [ and left, so parametrized. result points to the bracket, so this needs a right/left shift afterwards, like every other instr.
seekAny dir charMatch charOther t = go 0 (dir t)
where go n t
| n == 0 && isChar charMatch t = t
| isChar charMatch t = go (n - 1) (dir t)
| isChar charOther t = go (n + 1) (dir t)
| otherwise = go n (dir t)
seekRight :: Tape -> Tape
seekRight = seekAny right ']' '['
seekLeft :: Tape -> Tape
seekLeft = seekAny left '[' ']'
-- init for instruction tape
initStringTape :: [Char] -> Tape
initStringTape = initTape . map (toEnum . ord)
-- the whole brainfuck program / machine is a turingmachine, where both tapes are in a state which can change with every instr.
data TM = TM {dataTape :: Tape, insTape :: Tape}
instance Show TM where
show (TM d i) = "n" ++ show d ++ "n" ++ show' i
testtm = TM (initTape [2,7]) (initStringTape "[->+<]")
testtm2 = TM emptyTape (initStringTape "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.")
-- ============================================================================
-- Put the data into a stateful context using State (-transformers)
-- ============================================================================
-- TM state transformers
newtype State s a = State {runState :: s -> (a, s)}
-- State data constructor proxy
state :: (s -> (a, s)) -> State s a
state = State
-- canonical stuff
instance Functor (State s) where
fmap = liftM
instance Applicative (State s) where
pure = return
(<*>) = ap
instance Monad (State s) where
return x = state (s -> (x, s))
p >>= k = state $ s0 ->
let (x, s1) = runState p s0
in runState (k x) s1
-- ============================================================================
-- The brainfuck operations are stateful
-- ============================================================================
-- let the monadic functions return effects. can be extended as necessary
data Effect = None | Result Byte
isNone None = True
isNone _ = False
-- these seem a bit verbose.. is this necessary? is there a better way?
-- shift data tape
fRight :: TM -> (Effect, TM)
fRight (TM d i) = (None , TM (right d) i)
sRight = state fRight
fLeft :: TM -> (Effect, TM)
fLeft (TM d i) = (None, TM (left d) i)
sLeft = state fLeft
-- increment byte at ptr on data tape
fInc :: TM -> (Effect, TM)
fInc (TM d i) = (None, TM (inc d) i)
sInc = state fInc
fDec :: TM -> (Effect, TM)
fDec (TM d i) = (None, TM (dec d) i)
sDec = state fDec
-- read or write byte from/to data tape
fOutp :: TM -> (Effect, TM)
fOutp tm = (Result (readTape $ dataTape tm), tm)
sOutp = state fOutp
fInp :: Byte -> TM -> (Effect, TM)
fInp byte (TM d i) = (None, TM (writeTape byte d) i)
sInp byte = state (fInp byte)
-- seek the next [ or the previous ]
fFwd :: TM -> (Effect, TM)
fFwd (TM d i)
| isZero d = (None, TM d (seekRight i))
| otherwise = (None, (TM d i))
sFwd = state fFwd
fBwd :: TM -> (Effect, TM)
fBwd (TM d i)
| not $ isZero d = (None, TM d (seekLeft i))
| otherwise = (None, (TM d i))
sBwd = state fBwd
-- ============================================================================
--read instruction from instr tape
fReadInstr :: TM -> (Byte, TM)
fReadInstr tm = (readTape $ insTape tm, tm)
sReadInstr = state fReadInstr
--next instruction: shift instr tape
fNext :: TM -> ((), TM)
fNext (TM d i) = ((), TM d (right i))
sNext = state fNext
-- check if instruction tape is at the end
fCheck :: TM -> (Bool, TM)
fCheck (TM d i) = ((check i), TM d i)
where check tape = (readTape tape) == 0
sCheck = state fCheck
sCheckNot = do
res <- sCheck
return (not res)
-- ============================================================================
-- Putting it together: Run brainfuck turing machine step by step,
-- collecting potential results (Effect)
-- ============================================================================
-- from Control.Monad.Loops
whileM :: Monad m => m Bool -> m a -> m [a]
whileM = whileM'
whileM' :: (Monad m, MonadPlus f) => m Bool -> m a -> m (f a)
whileM' p f = go
where go = do
x <- p
if x
then do
x <- f
xs <- go
return (return x `mplus` xs)
else return mzero
-- from instruction character, resolve which action to take next.
chooseAction :: Enum a => a -> State TM Effect
chooseAction instr =
case chr . fromEnum $ instr of
'>' -> sRight
'<' -> sLeft
'+' -> sInc
'-' -> sDec
'.' -> sOutp --sOutp
',' -> return None --sInp -- to be combined with IO...
'[' -> sFwd
']' -> sBwd
_ -> return None
-- run the current step of the brainfuck program
stepTM :: State TM Effect
stepTM = do
instr <- sReadInstr
let next = chooseAction instr -- get statetransformer encoded by this char
res <- next -- run that statetransformer
sNext -- shift instruction tape pointer to right
return res
-- combine a list of possibly empty effects into a string
combineEffects :: [Effect] -> [Char]
combineEffects ms = go "" ms
where
go str [] = str
go str ((None):xs) = go str xs
go str ((Result byte):xs) = go (str ++ [(chr . fromEnum $ byte)]) xs
-- run a tm and print its result and states
runTM tm = (combineEffects (fst tup), (snd tup))
where
tup = runState (whileM sCheckNot stepTM) tm