Inspired by "An ASM Monad",
I've built a Haskell monad that produces
brainfuck programs.
The code for this monad is available
on hackage,
so cabal install brainfuck-monad
.
Here's a simple program written using this monad. See if you can guess what it might do:
import Control.Monad.BrainFuck
demo :: String
demo = brainfuckConstants $ \constants -> do
add 31
forever constants $ do
add 1
output
Here's the brainfuck code that demo
generates:
>+>++>+++>++++>+++++>++++++>+++++++>++++++++>++++++++++++++++++++++++++++++++<<<<<<<<[>>>>>>>>+.<<<<<<<<]
If you feed that into a brainfuck interpreter (I'm using hsbrainfuck
for my
testing), you'll find that it loops forever and prints out each character,
starting with space (32), in ASCIIbetical order.
The implementation is quite similar to the ASM monad. The main differences are that it builds a String, and that the BrainFuck monad keeps track of the current position of the data pointer (as brainfuck lacks any sane way to manipulate its instruction pointer).
newtype BrainFuck a = BrainFuck (DataPointer -> ([Char], DataPointer, a))
type DataPointer = Integer
-- Gets the current address of the data pointer.
addr :: BrainFuck DataPointer
addr = BrainFuck $ \loc -> ([], loc, loc)
Having the data pointer address available
allows writing some useful utility functions like this one,
which uses the next
(brainfuck opcode >
) and prev
(brainfuck opcode <
)
instructions.
-- Moves the data pointer to a specific address.
setAddr :: Integer -> BrainFuck ()
setAddr n = do
a <- addr
if a > n
then prev >> setAddr n
else if a < n
then next >> setAddr n
else return ()
Of course, brainfuck is a horrible language, designed to be nearly impossible to use. Here's the code to run a loop, but it's really hard to use this to build anything useful..
-- The loop is only entered if the byte at the data pointer is not zero.
-- On entry, the loop body is run, and then it loops when
-- the byte at the data pointer is not zero.
loopUnless0 :: BrainFuck () -> BrainFuck ()
loopUnless0 a = do
open
a
close
To tame brainfuck a bit, I decided to treat data addresses 0-8 as constants, which will contain the numbers 0-8. Otherwise, it's very hard to ensure that the data pointer is pointing at a nonzero number when you want to start a loop. (After all, brainfuck doesn't let you set data to some fixed value like 0 or 1!)
I wrote a little brainfuckConstants
that runs a BrainFuck
program with these constants set up at the beginning.
It just generates the brainfuck code for a series of ASCII art fishes:
>+>++>+++>++++>+++++>++++++>+++++++>++++++++>
With the fishes^Wconstants in place, it's possible to write a more useful
loop
. Notice how the data pointer location is saved at the beginning, and
restored inside the loop body. This ensures that the provided BrainFuck
action doesn't stomp on our constants.
-- Run an action in a loop, until it sets its data pointer to 0.
loop :: BrainFuck () -> BrainFuck ()
loop a = do
here <- addr
setAddr 1
loopUnless0 $ do
setAddr here
a
I haven't bothered to make sure that the constants are really constant,
but that could be done. It would just need a Control.Monad.BrainFuck.Safe
module, that uses a different monad, in which incr
and decr
and input
don't do anything when the data pointer is pointing at a constant.
Or, perhaps this could be statically checked at the type level, with
type level naturals. It's Haskell, we can make it safer if we want to. ;)
So, not only does this BrainFuck monad allow writing brainfuck code using crazy haskell syntax, instead of crazy brainfuck syntax, but it allows doing some higher-level programming, building up a useful(!?) library of BrainFuck combinators and using them to generate brainfuck code you'd not want to try to write by hand.
Of course, the real point is that "monad" and "brainfuck" so obviously belonged together that it would have been a crime not to write this.
I posted several examples, include a size-optimised "hello world" program in this thread: https://identi.ca/joeyh/note/htuYlTRfRnKLGud0xCOcmg
… I had wondered.
And it’s probably saying something about me that I find the brainfuck output more legible than the Haskell input ☺
(But only that I speak only one of the two languages.)
Instead of the constants hack, it's useful to define a stack-based function call model in brainfuck. This alloc higher-level function implements that, and is simplifying the code significantly.
Using alloc, we can build a nicer loopFrom, etc.