I'm writing a brainfuck interpreter in Haskell, and I came up with what I believe to be a very interesting description of a program:
data Program m = Instruction (m ()) (Program m)
| Control (m (Program m))
| Halt
However, it's tricky to parse a textual representation of a brainfuck program into this data type. The problem arises with trying to correctly parse square brackets, because there is some knot-tying to do so that the final Instruction
inside a loop links to the loop's Control
again.
A bit more preliminary information. See this version on the github repo for all the details.
type TapeM = StateT Tape IO
type TapeP = Program TapeM
type TapeC = Cont TapeP
branch :: Monad m => m Bool -> Program m -> Program m -> Program m
branch cond trueBranch falseBranch =
Control ((\b -> if b then trueBranch else falseBranch) `liftM` cond)
loopControl :: TapeP -> TapeP -> TapeP
loopControl = branch (not <$> is0)
Here's what I tried:
toProgram :: String -> TapeP
toProgram = (`runCont` id) . toProgramStep
liftI :: TapeM () -> String -> TapeC TapeP
liftI i cs = Instruction i <$> toProgramStep cs
toProgramStep :: String -> TapeC TapeP
toProgramStep ('>':cs) = liftI right cs
-- similarly for other instructions
toProgramStep ('[':cs) = push (toProgramStep cs)
toProgramStep (']':cs) = pop (toProgramStep cs)
push :: TapeC TapeP -> TapeC TapeP
push mcontinue = do
continue <- mcontinue
cont (\breakMake -> loopControl continue (breakMake continue))
pop :: TapeC TapeP -> TapeC TapeP
pop mbreak = do
break <- mbreak
cont (\continueMake -> loopControl (continueMake break) break)
I figured I could somehow use continuations to communicate information from the '['
case to the ']'
case and vice-versa, but I don't have a firm enough grasp of Cont to actually do anything besides assemble wild guesses of something that looks like it might work, as seen above with push
and pop
. This compiles and runs, but the results are garbage.
Can Cont
be used to tie the knot appropriately for this situation? If not, then what technique should I use to implement toProgram
?
Note 1: I previously had a subtle logic error: loopControl = branch is0
had the Bools reversed.
Note 2: I managed to use MonadFix
(as suggested by jberryman) with State
to come up with a solution (see the current state of the github repository). I'd still like to know how this could be done with Cont
instead.
Note 3: My Racketeer mentor put a similar Racket program together for me (see all revisions). Can his pipe/pipe-out technique be translated into Haskell using Cont
?
tl;dr I managed to do this using MonadFix, and someone else managed to do it using Racket's continuation combinators. I'm pretty sure this can be done with Cont
in Haskell. Can you show me how?
Cont
? Couldn't you just count the number of instructions to jump, perhaps doing a mulit-pass parse, where the extra passes associate the number of instructions to jump (along with direction) with the'['
and']'
characters? That is,[Char] -> [JumpInfo Char]
and then use your parser on the result, wheredata JumpInfo = JumpInfo Char (Maybe Integer)
. – HydrostatCont
than I am in implementing this particular interpreter. – Hazan]
before you've encountered any[
? – ScrotumtoProgram
to beString -> Maybe TapeP
. – HazantoProgram :: String -> (TapeP, String)
that returned the unconsumed string in the case of failure, so e.g.toProgram ">]>>" = (Instruction right Halt, "]>>")
. Then when encountering a[
, you just parse the next bit of the program, and check the unconsumed string starts with]
, then glue the bits together. I tried to write an answer in this style, but it turned out to be fiddlier than I expected, so maybe this won't work out, I'm not sure. – Scrotum