A simple brainf*** interpreter in Haskell

Multi tool use
Multi tool use












5














I am very new to Haskell and come from languages like C++, although I do have some experience with Scheme. Here, I wrote a simple brainf*** interpreter, which so far is my largest project. I followed a tutorial for some of the code (such as the Tape data type), but did most of this on my own. I have one major concern with my approach, which is the way I handle the loops (which in brainf*** are written with and repeat until the current cell in the tape is zero. In my implementation, I used the loop as a parameter to the main recursive function, which is called if the current instruction is ] and the current cell is not 0. Since these loops modify the tape, I had to make them return an instance of Tape that would replace the current one. However, since the function also handles IO (and therefore must return an instance of IO), I made it return an IO (Tape Int) which is then unpacked in a do block. This all felt very messy and hacky, so I would really appreciate any help from more experienced Haskell developers.



import Data.Maybe

--The Tape data type and functions

data Tape a = Tape [a] a [a]

newTape :: a -> Tape a
newTape x = Tape r x r
where r = repeat x

moveLeft :: Tape a -> Tape a
moveLeft (Tape (l:ls) x rs) = Tape ls l (x:rs)

moveRight :: Tape a -> Tape a
moveRight (Tape ls x (r:rs)) = Tape (x:ls) r rs

--The Brainf*** instruction data types

data BfInstruction
= MovLeft
| MovRight
| Increment
| Decrement
| Output
| Input
| BeginLoop
| EndLoop
deriving (Show, Eq)

type BfProgram = [BfInstruction]

--Convert string to BfProgram

parseBf :: String -> BfProgram
parseBf = mapMaybe parse
where
parse :: Char -> Maybe BfInstruction
parse x = case x of
'<' -> Just MovLeft
'>' -> Just MovRight
'+' -> Just Increment
'-' -> Just Decrement
',' -> Just Input
'.' -> Just Output
'[' -> Just BeginLoop
']' -> Just EndLoop
x -> Nothing --anything but the above chars is a comment

--Main running function

runBf :: String -> IO ()
runBf p = runBf' (parseBf p) (newTape 0) >> return ()
where
runBf' :: BfProgram -> Tape Int -> BfProgram -> IO (Tape Int)
runBf' tape _ = return tape
runBf' prog@(p:ps) tape@(Tape ls x rs) loop = case p of
MovLeft -> advance prog (moveLeft tape)
MovRight -> advance prog (moveRight tape)
Increment -> advance prog (Tape ls (x+1) rs)
Decrement -> advance prog (Tape ls (x-1) rs)
Input -> do
char <- getChar
advance prog (Tape ls (fromEnum char) rs)
Output -> putChar (toEnum x) >> advance prog tape
BeginLoop ->
let lp = getLoop 1 ps
in runBf' (length lp `drop` ps) tape lp --Drop so that we are at the ] now
EndLoop ->
if x /= 0
then do
lt <- runBf' loop tape
runBf' prog lt loop --Copy the tape from the result of the loop into next iteration
else advance prog tape

advance :: BfProgram -> Tape Int -> IO (Tape Int)
advance (p:ps) tape = runBf' ps tape

getLoop :: Int -> BfProgram -> BfProgram
getLoop _ = error "Mismatched brackets in BF program"
getLoop 1 (EndLoop:ps) =
getLoop n (p:ps) = p:case p of
BeginLoop -> getLoop (n + 1) ps
EndLoop -> getLoop (n - 1) ps
_ -> getLoop n ps

--Simple IO

main = do
program <- readFile "program.bf"
runBf program









share|improve this question
























  • Welcome to Code Review. That almost looks like literate Haskell, did you mean to split your code into several snippets?
    – Zeta
    Aug 15 at 19:29










  • I did? I separated my data type definitions and functions. What should I have done?
    – mrFoobles
    Aug 15 at 19:40










  • You can keep it like this, but it's usually better if code is kept copyable (e.g. one file = one code block) so that interested reviewers can test your program (it's not a required, though). Literate Haskell looks very similar to your presentation style, so you might be interested in it.
    – Zeta
    Aug 15 at 19:42












  • Ok thank you. I looked around and saw some other people doing it, so I split mine up too. Just adjusted it.
    – mrFoobles
    Aug 15 at 19:44










  • It's not required per-se to keep the code together, you can keep the code split for presentation purposes, sorry if my comment made it sound like a requirement. Either way, I hope you get some nice reviews.
    – Zeta
    Aug 15 at 19:46
















5














I am very new to Haskell and come from languages like C++, although I do have some experience with Scheme. Here, I wrote a simple brainf*** interpreter, which so far is my largest project. I followed a tutorial for some of the code (such as the Tape data type), but did most of this on my own. I have one major concern with my approach, which is the way I handle the loops (which in brainf*** are written with and repeat until the current cell in the tape is zero. In my implementation, I used the loop as a parameter to the main recursive function, which is called if the current instruction is ] and the current cell is not 0. Since these loops modify the tape, I had to make them return an instance of Tape that would replace the current one. However, since the function also handles IO (and therefore must return an instance of IO), I made it return an IO (Tape Int) which is then unpacked in a do block. This all felt very messy and hacky, so I would really appreciate any help from more experienced Haskell developers.



import Data.Maybe

--The Tape data type and functions

data Tape a = Tape [a] a [a]

newTape :: a -> Tape a
newTape x = Tape r x r
where r = repeat x

moveLeft :: Tape a -> Tape a
moveLeft (Tape (l:ls) x rs) = Tape ls l (x:rs)

moveRight :: Tape a -> Tape a
moveRight (Tape ls x (r:rs)) = Tape (x:ls) r rs

--The Brainf*** instruction data types

data BfInstruction
= MovLeft
| MovRight
| Increment
| Decrement
| Output
| Input
| BeginLoop
| EndLoop
deriving (Show, Eq)

type BfProgram = [BfInstruction]

--Convert string to BfProgram

parseBf :: String -> BfProgram
parseBf = mapMaybe parse
where
parse :: Char -> Maybe BfInstruction
parse x = case x of
'<' -> Just MovLeft
'>' -> Just MovRight
'+' -> Just Increment
'-' -> Just Decrement
',' -> Just Input
'.' -> Just Output
'[' -> Just BeginLoop
']' -> Just EndLoop
x -> Nothing --anything but the above chars is a comment

--Main running function

runBf :: String -> IO ()
runBf p = runBf' (parseBf p) (newTape 0) >> return ()
where
runBf' :: BfProgram -> Tape Int -> BfProgram -> IO (Tape Int)
runBf' tape _ = return tape
runBf' prog@(p:ps) tape@(Tape ls x rs) loop = case p of
MovLeft -> advance prog (moveLeft tape)
MovRight -> advance prog (moveRight tape)
Increment -> advance prog (Tape ls (x+1) rs)
Decrement -> advance prog (Tape ls (x-1) rs)
Input -> do
char <- getChar
advance prog (Tape ls (fromEnum char) rs)
Output -> putChar (toEnum x) >> advance prog tape
BeginLoop ->
let lp = getLoop 1 ps
in runBf' (length lp `drop` ps) tape lp --Drop so that we are at the ] now
EndLoop ->
if x /= 0
then do
lt <- runBf' loop tape
runBf' prog lt loop --Copy the tape from the result of the loop into next iteration
else advance prog tape

advance :: BfProgram -> Tape Int -> IO (Tape Int)
advance (p:ps) tape = runBf' ps tape

getLoop :: Int -> BfProgram -> BfProgram
getLoop _ = error "Mismatched brackets in BF program"
getLoop 1 (EndLoop:ps) =
getLoop n (p:ps) = p:case p of
BeginLoop -> getLoop (n + 1) ps
EndLoop -> getLoop (n - 1) ps
_ -> getLoop n ps

--Simple IO

main = do
program <- readFile "program.bf"
runBf program









share|improve this question
























  • Welcome to Code Review. That almost looks like literate Haskell, did you mean to split your code into several snippets?
    – Zeta
    Aug 15 at 19:29










  • I did? I separated my data type definitions and functions. What should I have done?
    – mrFoobles
    Aug 15 at 19:40










  • You can keep it like this, but it's usually better if code is kept copyable (e.g. one file = one code block) so that interested reviewers can test your program (it's not a required, though). Literate Haskell looks very similar to your presentation style, so you might be interested in it.
    – Zeta
    Aug 15 at 19:42












  • Ok thank you. I looked around and saw some other people doing it, so I split mine up too. Just adjusted it.
    – mrFoobles
    Aug 15 at 19:44










  • It's not required per-se to keep the code together, you can keep the code split for presentation purposes, sorry if my comment made it sound like a requirement. Either way, I hope you get some nice reviews.
    – Zeta
    Aug 15 at 19:46














5












5








5


1





I am very new to Haskell and come from languages like C++, although I do have some experience with Scheme. Here, I wrote a simple brainf*** interpreter, which so far is my largest project. I followed a tutorial for some of the code (such as the Tape data type), but did most of this on my own. I have one major concern with my approach, which is the way I handle the loops (which in brainf*** are written with and repeat until the current cell in the tape is zero. In my implementation, I used the loop as a parameter to the main recursive function, which is called if the current instruction is ] and the current cell is not 0. Since these loops modify the tape, I had to make them return an instance of Tape that would replace the current one. However, since the function also handles IO (and therefore must return an instance of IO), I made it return an IO (Tape Int) which is then unpacked in a do block. This all felt very messy and hacky, so I would really appreciate any help from more experienced Haskell developers.



import Data.Maybe

--The Tape data type and functions

data Tape a = Tape [a] a [a]

newTape :: a -> Tape a
newTape x = Tape r x r
where r = repeat x

moveLeft :: Tape a -> Tape a
moveLeft (Tape (l:ls) x rs) = Tape ls l (x:rs)

moveRight :: Tape a -> Tape a
moveRight (Tape ls x (r:rs)) = Tape (x:ls) r rs

--The Brainf*** instruction data types

data BfInstruction
= MovLeft
| MovRight
| Increment
| Decrement
| Output
| Input
| BeginLoop
| EndLoop
deriving (Show, Eq)

type BfProgram = [BfInstruction]

--Convert string to BfProgram

parseBf :: String -> BfProgram
parseBf = mapMaybe parse
where
parse :: Char -> Maybe BfInstruction
parse x = case x of
'<' -> Just MovLeft
'>' -> Just MovRight
'+' -> Just Increment
'-' -> Just Decrement
',' -> Just Input
'.' -> Just Output
'[' -> Just BeginLoop
']' -> Just EndLoop
x -> Nothing --anything but the above chars is a comment

--Main running function

runBf :: String -> IO ()
runBf p = runBf' (parseBf p) (newTape 0) >> return ()
where
runBf' :: BfProgram -> Tape Int -> BfProgram -> IO (Tape Int)
runBf' tape _ = return tape
runBf' prog@(p:ps) tape@(Tape ls x rs) loop = case p of
MovLeft -> advance prog (moveLeft tape)
MovRight -> advance prog (moveRight tape)
Increment -> advance prog (Tape ls (x+1) rs)
Decrement -> advance prog (Tape ls (x-1) rs)
Input -> do
char <- getChar
advance prog (Tape ls (fromEnum char) rs)
Output -> putChar (toEnum x) >> advance prog tape
BeginLoop ->
let lp = getLoop 1 ps
in runBf' (length lp `drop` ps) tape lp --Drop so that we are at the ] now
EndLoop ->
if x /= 0
then do
lt <- runBf' loop tape
runBf' prog lt loop --Copy the tape from the result of the loop into next iteration
else advance prog tape

advance :: BfProgram -> Tape Int -> IO (Tape Int)
advance (p:ps) tape = runBf' ps tape

getLoop :: Int -> BfProgram -> BfProgram
getLoop _ = error "Mismatched brackets in BF program"
getLoop 1 (EndLoop:ps) =
getLoop n (p:ps) = p:case p of
BeginLoop -> getLoop (n + 1) ps
EndLoop -> getLoop (n - 1) ps
_ -> getLoop n ps

--Simple IO

main = do
program <- readFile "program.bf"
runBf program









share|improve this question















I am very new to Haskell and come from languages like C++, although I do have some experience with Scheme. Here, I wrote a simple brainf*** interpreter, which so far is my largest project. I followed a tutorial for some of the code (such as the Tape data type), but did most of this on my own. I have one major concern with my approach, which is the way I handle the loops (which in brainf*** are written with and repeat until the current cell in the tape is zero. In my implementation, I used the loop as a parameter to the main recursive function, which is called if the current instruction is ] and the current cell is not 0. Since these loops modify the tape, I had to make them return an instance of Tape that would replace the current one. However, since the function also handles IO (and therefore must return an instance of IO), I made it return an IO (Tape Int) which is then unpacked in a do block. This all felt very messy and hacky, so I would really appreciate any help from more experienced Haskell developers.



import Data.Maybe

--The Tape data type and functions

data Tape a = Tape [a] a [a]

newTape :: a -> Tape a
newTape x = Tape r x r
where r = repeat x

moveLeft :: Tape a -> Tape a
moveLeft (Tape (l:ls) x rs) = Tape ls l (x:rs)

moveRight :: Tape a -> Tape a
moveRight (Tape ls x (r:rs)) = Tape (x:ls) r rs

--The Brainf*** instruction data types

data BfInstruction
= MovLeft
| MovRight
| Increment
| Decrement
| Output
| Input
| BeginLoop
| EndLoop
deriving (Show, Eq)

type BfProgram = [BfInstruction]

--Convert string to BfProgram

parseBf :: String -> BfProgram
parseBf = mapMaybe parse
where
parse :: Char -> Maybe BfInstruction
parse x = case x of
'<' -> Just MovLeft
'>' -> Just MovRight
'+' -> Just Increment
'-' -> Just Decrement
',' -> Just Input
'.' -> Just Output
'[' -> Just BeginLoop
']' -> Just EndLoop
x -> Nothing --anything but the above chars is a comment

--Main running function

runBf :: String -> IO ()
runBf p = runBf' (parseBf p) (newTape 0) >> return ()
where
runBf' :: BfProgram -> Tape Int -> BfProgram -> IO (Tape Int)
runBf' tape _ = return tape
runBf' prog@(p:ps) tape@(Tape ls x rs) loop = case p of
MovLeft -> advance prog (moveLeft tape)
MovRight -> advance prog (moveRight tape)
Increment -> advance prog (Tape ls (x+1) rs)
Decrement -> advance prog (Tape ls (x-1) rs)
Input -> do
char <- getChar
advance prog (Tape ls (fromEnum char) rs)
Output -> putChar (toEnum x) >> advance prog tape
BeginLoop ->
let lp = getLoop 1 ps
in runBf' (length lp `drop` ps) tape lp --Drop so that we are at the ] now
EndLoop ->
if x /= 0
then do
lt <- runBf' loop tape
runBf' prog lt loop --Copy the tape from the result of the loop into next iteration
else advance prog tape

advance :: BfProgram -> Tape Int -> IO (Tape Int)
advance (p:ps) tape = runBf' ps tape

getLoop :: Int -> BfProgram -> BfProgram
getLoop _ = error "Mismatched brackets in BF program"
getLoop 1 (EndLoop:ps) =
getLoop n (p:ps) = p:case p of
BeginLoop -> getLoop (n + 1) ps
EndLoop -> getLoop (n - 1) ps
_ -> getLoop n ps

--Simple IO

main = do
program <- readFile "program.bf"
runBf program






beginner haskell interpreter brainfuck






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Aug 15 at 19:46

























asked Aug 15 at 18:54









mrFoobles

262




262












  • Welcome to Code Review. That almost looks like literate Haskell, did you mean to split your code into several snippets?
    – Zeta
    Aug 15 at 19:29










  • I did? I separated my data type definitions and functions. What should I have done?
    – mrFoobles
    Aug 15 at 19:40










  • You can keep it like this, but it's usually better if code is kept copyable (e.g. one file = one code block) so that interested reviewers can test your program (it's not a required, though). Literate Haskell looks very similar to your presentation style, so you might be interested in it.
    – Zeta
    Aug 15 at 19:42












  • Ok thank you. I looked around and saw some other people doing it, so I split mine up too. Just adjusted it.
    – mrFoobles
    Aug 15 at 19:44










  • It's not required per-se to keep the code together, you can keep the code split for presentation purposes, sorry if my comment made it sound like a requirement. Either way, I hope you get some nice reviews.
    – Zeta
    Aug 15 at 19:46


















  • Welcome to Code Review. That almost looks like literate Haskell, did you mean to split your code into several snippets?
    – Zeta
    Aug 15 at 19:29










  • I did? I separated my data type definitions and functions. What should I have done?
    – mrFoobles
    Aug 15 at 19:40










  • You can keep it like this, but it's usually better if code is kept copyable (e.g. one file = one code block) so that interested reviewers can test your program (it's not a required, though). Literate Haskell looks very similar to your presentation style, so you might be interested in it.
    – Zeta
    Aug 15 at 19:42












  • Ok thank you. I looked around and saw some other people doing it, so I split mine up too. Just adjusted it.
    – mrFoobles
    Aug 15 at 19:44










  • It's not required per-se to keep the code together, you can keep the code split for presentation purposes, sorry if my comment made it sound like a requirement. Either way, I hope you get some nice reviews.
    – Zeta
    Aug 15 at 19:46
















Welcome to Code Review. That almost looks like literate Haskell, did you mean to split your code into several snippets?
– Zeta
Aug 15 at 19:29




Welcome to Code Review. That almost looks like literate Haskell, did you mean to split your code into several snippets?
– Zeta
Aug 15 at 19:29












I did? I separated my data type definitions and functions. What should I have done?
– mrFoobles
Aug 15 at 19:40




I did? I separated my data type definitions and functions. What should I have done?
– mrFoobles
Aug 15 at 19:40












You can keep it like this, but it's usually better if code is kept copyable (e.g. one file = one code block) so that interested reviewers can test your program (it's not a required, though). Literate Haskell looks very similar to your presentation style, so you might be interested in it.
– Zeta
Aug 15 at 19:42






You can keep it like this, but it's usually better if code is kept copyable (e.g. one file = one code block) so that interested reviewers can test your program (it's not a required, though). Literate Haskell looks very similar to your presentation style, so you might be interested in it.
– Zeta
Aug 15 at 19:42














Ok thank you. I looked around and saw some other people doing it, so I split mine up too. Just adjusted it.
– mrFoobles
Aug 15 at 19:44




Ok thank you. I looked around and saw some other people doing it, so I split mine up too. Just adjusted it.
– mrFoobles
Aug 15 at 19:44












It's not required per-se to keep the code together, you can keep the code split for presentation purposes, sorry if my comment made it sound like a requirement. Either way, I hope you get some nice reviews.
– Zeta
Aug 15 at 19:46




It's not required per-se to keep the code together, you can keep the code split for presentation purposes, sorry if my comment made it sound like a requirement. Either way, I hope you get some nice reviews.
– Zeta
Aug 15 at 19:46










1 Answer
1






active

oldest

votes


















0














Well done. Overall, there are no big flaws, just some minor issues.



Type annotations



While it's great that all top-level functions have proper type signatures, the local bindings inside those functions usually don't. After all, their types should get inferred, e.g.



example :: [Int] -> [Int]
example = map inc
where
inc x = 1 + x


doesn't need a type signature since x's type is already fixed to Int. It makes refactoring also a lot easier if we change the type later. If we started with



example :: [Int] -> [Int]
example = map inc
where
inc :: Int -> Int
inc x = 1 + x


and later want to generalize, we might forget the second type signature and end up with an error message:



example :: Num a => [a] -> [a]
example = map inc
where
inc :: Int -> Int -- whoops, GHC will yell about that
inc x = 1 + x


Therefore, type signatures for local functions are usually not written out. There are some instances where they're necessary, but that's usually with RankNTypes or other extensions.



The tape



The tape works well, and is pretty much how you would expect it.



Infinite tapes and debugging



That being said, an infinite tape has the slight inconvenience that you can never inspect it for debugging purposes.



Also, if you ever create a module from your code, you must not export the Tape data constructor, as it would enable Tape 0 and therefore break assertions.



A finite tape circumvents those issues, but needs slightly more effort in the movements.



Working on the current value



In runBf we can find several spots where we advance the program after we worked on the current value, e.g.:



        Increment -> advance prog (Tape ls (x+1) rs)
Decrement -> advance prog (Tape ls (x-1) rs)


That's now a possible source of errors, since we could have used



        Increment -> advance prog (Tape ls (x+1) ls)
Decrement -> advance prog (Tape ls (x-1) ls)


by accident. A small helper can prevent that issue:



onCurrent :: (a -> a) -> Tape a -> Tape a
onCurrent f (Tape ls x rs) = Tape ls (f x) rs

current :: Tape a -> a
current (Tape _ x _ ) = x


Then we end up with



        MovLeft   -> advance prog (moveLeft tape)
MovRight -> advance prog (moveRight tape)
Increment -> advance prog (onCurrent (+1) tape)
Decrement -> advance prog (onCurrent (subtract 1) tape)
Input -> do
char <- getChar
advance prog (onCurrent (const (fromEnum char)) tape)


Naming and scope



As neither advance nor getLoop use any of the bindings in their scope, they're candidates for top-level functions.



runBf' can be called go or another short name. Calling the inner worker just go is really common and won't alienate other readers.



Make interfaces hard to use wrong



getLoop uses an Int as first argument that's not properly documented. Types only go so far as documentation, and we could accidentally use getLoop 0 in BeginLoop.



Instead, we should make it impossible to misuse getLoop:



getLoop :: BfProgram -> BfProgram
getLoop = go 1
where
go _ = error "Mismatched brackets in BF program"
go 1 (EndLoop:ps) =
go n (p:ps) = p:case p of
BeginLoop -> go (n + 1) ps
EndLoop -> go (n - 1) ps
_ -> go n ps


Similarly, runBf should probably take a BfProgram, not an arbitrary String, as this doesn't decrease the strength of your program, we can recreate the previous behaviour with



runBf . parseBf


However, speaking of parsing…



Loop validation



A drawback with our current BfProgram is that we might end up with mismatched brackets, e.g.



parseBf "]["


parses fine and leads to a runtime error. However, we could easily detect that during parsing. Our parseBf needs a way to report errors:



type ParserError = String

parseBf :: String -> Either ParserError BfProgram
parseBf = go
where
go = Right
go (x:xs) = case x of
'<' -> MovLeft <$:> go xs
'>' -> MovRight <$:> go xs
'+' -> Increment <$:> go xs
'-' -> Decrement <$:> go xs
',' -> Input <$:> go xs
'.' -> Output <$:> go xs
'[' -> -- exercise ; use `getLoop`-like function
']' -> -- exercise ; easier if previous one done correctly.
x -> go xs
x <$:> xs = fmap (x:) xs


but afterwards, we can be sure that parseBf only returns BfPrograms with valid brackets.



Unfortunately, we still need to use getLoop, as BeginLoop and EndLoop are still in our instruction set. If we change the instruction set, we can get rid of that too:



data BfInstruction 
= MovLeft
| MovRight
| Increment
| Decrement
| Output
| Input
| Loop BfProgram
deriving (Show, Eq)


I go into more details in some of my previous Bf reviews, feel free to read them if you get stuck on Loop.



Final remarks



Other than the usual re-evaluation of loops (which is a common scenario in Haskell Bf interpreters), your code was fine, so all the issues are really minor. Again: well done.






share|improve this answer





















    Your Answer





    StackExchange.ifUsing("editor", function () {
    return StackExchange.using("mathjaxEditing", function () {
    StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
    StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
    });
    });
    }, "mathjax-editing");

    StackExchange.ifUsing("editor", function () {
    StackExchange.using("externalEditor", function () {
    StackExchange.using("snippets", function () {
    StackExchange.snippets.init();
    });
    });
    }, "code-snippets");

    StackExchange.ready(function() {
    var channelOptions = {
    tags: "".split(" "),
    id: "196"
    };
    initTagRenderer("".split(" "), "".split(" "), channelOptions);

    StackExchange.using("externalEditor", function() {
    // Have to fire editor after snippets, if snippets enabled
    if (StackExchange.settings.snippets.snippetsEnabled) {
    StackExchange.using("snippets", function() {
    createEditor();
    });
    }
    else {
    createEditor();
    }
    });

    function createEditor() {
    StackExchange.prepareEditor({
    heartbeatType: 'answer',
    autoActivateHeartbeat: false,
    convertImagesToLinks: false,
    noModals: true,
    showLowRepImageUploadWarning: true,
    reputationToPostImages: null,
    bindNavPrevention: true,
    postfix: "",
    imageUploader: {
    brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
    contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
    allowUrls: true
    },
    onDemand: true,
    discardSelector: ".discard-answer"
    ,immediatelyShowMarkdownHelp:true
    });


    }
    });














    draft saved

    draft discarded


















    StackExchange.ready(
    function () {
    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f201746%2fa-simple-brainf-interpreter-in-haskell%23new-answer', 'question_page');
    }
    );

    Post as a guest















    Required, but never shown

























    1 Answer
    1






    active

    oldest

    votes








    1 Answer
    1






    active

    oldest

    votes









    active

    oldest

    votes






    active

    oldest

    votes









    0














    Well done. Overall, there are no big flaws, just some minor issues.



    Type annotations



    While it's great that all top-level functions have proper type signatures, the local bindings inside those functions usually don't. After all, their types should get inferred, e.g.



    example :: [Int] -> [Int]
    example = map inc
    where
    inc x = 1 + x


    doesn't need a type signature since x's type is already fixed to Int. It makes refactoring also a lot easier if we change the type later. If we started with



    example :: [Int] -> [Int]
    example = map inc
    where
    inc :: Int -> Int
    inc x = 1 + x


    and later want to generalize, we might forget the second type signature and end up with an error message:



    example :: Num a => [a] -> [a]
    example = map inc
    where
    inc :: Int -> Int -- whoops, GHC will yell about that
    inc x = 1 + x


    Therefore, type signatures for local functions are usually not written out. There are some instances where they're necessary, but that's usually with RankNTypes or other extensions.



    The tape



    The tape works well, and is pretty much how you would expect it.



    Infinite tapes and debugging



    That being said, an infinite tape has the slight inconvenience that you can never inspect it for debugging purposes.



    Also, if you ever create a module from your code, you must not export the Tape data constructor, as it would enable Tape 0 and therefore break assertions.



    A finite tape circumvents those issues, but needs slightly more effort in the movements.



    Working on the current value



    In runBf we can find several spots where we advance the program after we worked on the current value, e.g.:



            Increment -> advance prog (Tape ls (x+1) rs)
    Decrement -> advance prog (Tape ls (x-1) rs)


    That's now a possible source of errors, since we could have used



            Increment -> advance prog (Tape ls (x+1) ls)
    Decrement -> advance prog (Tape ls (x-1) ls)


    by accident. A small helper can prevent that issue:



    onCurrent :: (a -> a) -> Tape a -> Tape a
    onCurrent f (Tape ls x rs) = Tape ls (f x) rs

    current :: Tape a -> a
    current (Tape _ x _ ) = x


    Then we end up with



            MovLeft   -> advance prog (moveLeft tape)
    MovRight -> advance prog (moveRight tape)
    Increment -> advance prog (onCurrent (+1) tape)
    Decrement -> advance prog (onCurrent (subtract 1) tape)
    Input -> do
    char <- getChar
    advance prog (onCurrent (const (fromEnum char)) tape)


    Naming and scope



    As neither advance nor getLoop use any of the bindings in their scope, they're candidates for top-level functions.



    runBf' can be called go or another short name. Calling the inner worker just go is really common and won't alienate other readers.



    Make interfaces hard to use wrong



    getLoop uses an Int as first argument that's not properly documented. Types only go so far as documentation, and we could accidentally use getLoop 0 in BeginLoop.



    Instead, we should make it impossible to misuse getLoop:



    getLoop :: BfProgram -> BfProgram
    getLoop = go 1
    where
    go _ = error "Mismatched brackets in BF program"
    go 1 (EndLoop:ps) =
    go n (p:ps) = p:case p of
    BeginLoop -> go (n + 1) ps
    EndLoop -> go (n - 1) ps
    _ -> go n ps


    Similarly, runBf should probably take a BfProgram, not an arbitrary String, as this doesn't decrease the strength of your program, we can recreate the previous behaviour with



    runBf . parseBf


    However, speaking of parsing…



    Loop validation



    A drawback with our current BfProgram is that we might end up with mismatched brackets, e.g.



    parseBf "]["


    parses fine and leads to a runtime error. However, we could easily detect that during parsing. Our parseBf needs a way to report errors:



    type ParserError = String

    parseBf :: String -> Either ParserError BfProgram
    parseBf = go
    where
    go = Right
    go (x:xs) = case x of
    '<' -> MovLeft <$:> go xs
    '>' -> MovRight <$:> go xs
    '+' -> Increment <$:> go xs
    '-' -> Decrement <$:> go xs
    ',' -> Input <$:> go xs
    '.' -> Output <$:> go xs
    '[' -> -- exercise ; use `getLoop`-like function
    ']' -> -- exercise ; easier if previous one done correctly.
    x -> go xs
    x <$:> xs = fmap (x:) xs


    but afterwards, we can be sure that parseBf only returns BfPrograms with valid brackets.



    Unfortunately, we still need to use getLoop, as BeginLoop and EndLoop are still in our instruction set. If we change the instruction set, we can get rid of that too:



    data BfInstruction 
    = MovLeft
    | MovRight
    | Increment
    | Decrement
    | Output
    | Input
    | Loop BfProgram
    deriving (Show, Eq)


    I go into more details in some of my previous Bf reviews, feel free to read them if you get stuck on Loop.



    Final remarks



    Other than the usual re-evaluation of loops (which is a common scenario in Haskell Bf interpreters), your code was fine, so all the issues are really minor. Again: well done.






    share|improve this answer


























      0














      Well done. Overall, there are no big flaws, just some minor issues.



      Type annotations



      While it's great that all top-level functions have proper type signatures, the local bindings inside those functions usually don't. After all, their types should get inferred, e.g.



      example :: [Int] -> [Int]
      example = map inc
      where
      inc x = 1 + x


      doesn't need a type signature since x's type is already fixed to Int. It makes refactoring also a lot easier if we change the type later. If we started with



      example :: [Int] -> [Int]
      example = map inc
      where
      inc :: Int -> Int
      inc x = 1 + x


      and later want to generalize, we might forget the second type signature and end up with an error message:



      example :: Num a => [a] -> [a]
      example = map inc
      where
      inc :: Int -> Int -- whoops, GHC will yell about that
      inc x = 1 + x


      Therefore, type signatures for local functions are usually not written out. There are some instances where they're necessary, but that's usually with RankNTypes or other extensions.



      The tape



      The tape works well, and is pretty much how you would expect it.



      Infinite tapes and debugging



      That being said, an infinite tape has the slight inconvenience that you can never inspect it for debugging purposes.



      Also, if you ever create a module from your code, you must not export the Tape data constructor, as it would enable Tape 0 and therefore break assertions.



      A finite tape circumvents those issues, but needs slightly more effort in the movements.



      Working on the current value



      In runBf we can find several spots where we advance the program after we worked on the current value, e.g.:



              Increment -> advance prog (Tape ls (x+1) rs)
      Decrement -> advance prog (Tape ls (x-1) rs)


      That's now a possible source of errors, since we could have used



              Increment -> advance prog (Tape ls (x+1) ls)
      Decrement -> advance prog (Tape ls (x-1) ls)


      by accident. A small helper can prevent that issue:



      onCurrent :: (a -> a) -> Tape a -> Tape a
      onCurrent f (Tape ls x rs) = Tape ls (f x) rs

      current :: Tape a -> a
      current (Tape _ x _ ) = x


      Then we end up with



              MovLeft   -> advance prog (moveLeft tape)
      MovRight -> advance prog (moveRight tape)
      Increment -> advance prog (onCurrent (+1) tape)
      Decrement -> advance prog (onCurrent (subtract 1) tape)
      Input -> do
      char <- getChar
      advance prog (onCurrent (const (fromEnum char)) tape)


      Naming and scope



      As neither advance nor getLoop use any of the bindings in their scope, they're candidates for top-level functions.



      runBf' can be called go or another short name. Calling the inner worker just go is really common and won't alienate other readers.



      Make interfaces hard to use wrong



      getLoop uses an Int as first argument that's not properly documented. Types only go so far as documentation, and we could accidentally use getLoop 0 in BeginLoop.



      Instead, we should make it impossible to misuse getLoop:



      getLoop :: BfProgram -> BfProgram
      getLoop = go 1
      where
      go _ = error "Mismatched brackets in BF program"
      go 1 (EndLoop:ps) =
      go n (p:ps) = p:case p of
      BeginLoop -> go (n + 1) ps
      EndLoop -> go (n - 1) ps
      _ -> go n ps


      Similarly, runBf should probably take a BfProgram, not an arbitrary String, as this doesn't decrease the strength of your program, we can recreate the previous behaviour with



      runBf . parseBf


      However, speaking of parsing…



      Loop validation



      A drawback with our current BfProgram is that we might end up with mismatched brackets, e.g.



      parseBf "]["


      parses fine and leads to a runtime error. However, we could easily detect that during parsing. Our parseBf needs a way to report errors:



      type ParserError = String

      parseBf :: String -> Either ParserError BfProgram
      parseBf = go
      where
      go = Right
      go (x:xs) = case x of
      '<' -> MovLeft <$:> go xs
      '>' -> MovRight <$:> go xs
      '+' -> Increment <$:> go xs
      '-' -> Decrement <$:> go xs
      ',' -> Input <$:> go xs
      '.' -> Output <$:> go xs
      '[' -> -- exercise ; use `getLoop`-like function
      ']' -> -- exercise ; easier if previous one done correctly.
      x -> go xs
      x <$:> xs = fmap (x:) xs


      but afterwards, we can be sure that parseBf only returns BfPrograms with valid brackets.



      Unfortunately, we still need to use getLoop, as BeginLoop and EndLoop are still in our instruction set. If we change the instruction set, we can get rid of that too:



      data BfInstruction 
      = MovLeft
      | MovRight
      | Increment
      | Decrement
      | Output
      | Input
      | Loop BfProgram
      deriving (Show, Eq)


      I go into more details in some of my previous Bf reviews, feel free to read them if you get stuck on Loop.



      Final remarks



      Other than the usual re-evaluation of loops (which is a common scenario in Haskell Bf interpreters), your code was fine, so all the issues are really minor. Again: well done.






      share|improve this answer
























        0












        0








        0






        Well done. Overall, there are no big flaws, just some minor issues.



        Type annotations



        While it's great that all top-level functions have proper type signatures, the local bindings inside those functions usually don't. After all, their types should get inferred, e.g.



        example :: [Int] -> [Int]
        example = map inc
        where
        inc x = 1 + x


        doesn't need a type signature since x's type is already fixed to Int. It makes refactoring also a lot easier if we change the type later. If we started with



        example :: [Int] -> [Int]
        example = map inc
        where
        inc :: Int -> Int
        inc x = 1 + x


        and later want to generalize, we might forget the second type signature and end up with an error message:



        example :: Num a => [a] -> [a]
        example = map inc
        where
        inc :: Int -> Int -- whoops, GHC will yell about that
        inc x = 1 + x


        Therefore, type signatures for local functions are usually not written out. There are some instances where they're necessary, but that's usually with RankNTypes or other extensions.



        The tape



        The tape works well, and is pretty much how you would expect it.



        Infinite tapes and debugging



        That being said, an infinite tape has the slight inconvenience that you can never inspect it for debugging purposes.



        Also, if you ever create a module from your code, you must not export the Tape data constructor, as it would enable Tape 0 and therefore break assertions.



        A finite tape circumvents those issues, but needs slightly more effort in the movements.



        Working on the current value



        In runBf we can find several spots where we advance the program after we worked on the current value, e.g.:



                Increment -> advance prog (Tape ls (x+1) rs)
        Decrement -> advance prog (Tape ls (x-1) rs)


        That's now a possible source of errors, since we could have used



                Increment -> advance prog (Tape ls (x+1) ls)
        Decrement -> advance prog (Tape ls (x-1) ls)


        by accident. A small helper can prevent that issue:



        onCurrent :: (a -> a) -> Tape a -> Tape a
        onCurrent f (Tape ls x rs) = Tape ls (f x) rs

        current :: Tape a -> a
        current (Tape _ x _ ) = x


        Then we end up with



                MovLeft   -> advance prog (moveLeft tape)
        MovRight -> advance prog (moveRight tape)
        Increment -> advance prog (onCurrent (+1) tape)
        Decrement -> advance prog (onCurrent (subtract 1) tape)
        Input -> do
        char <- getChar
        advance prog (onCurrent (const (fromEnum char)) tape)


        Naming and scope



        As neither advance nor getLoop use any of the bindings in their scope, they're candidates for top-level functions.



        runBf' can be called go or another short name. Calling the inner worker just go is really common and won't alienate other readers.



        Make interfaces hard to use wrong



        getLoop uses an Int as first argument that's not properly documented. Types only go so far as documentation, and we could accidentally use getLoop 0 in BeginLoop.



        Instead, we should make it impossible to misuse getLoop:



        getLoop :: BfProgram -> BfProgram
        getLoop = go 1
        where
        go _ = error "Mismatched brackets in BF program"
        go 1 (EndLoop:ps) =
        go n (p:ps) = p:case p of
        BeginLoop -> go (n + 1) ps
        EndLoop -> go (n - 1) ps
        _ -> go n ps


        Similarly, runBf should probably take a BfProgram, not an arbitrary String, as this doesn't decrease the strength of your program, we can recreate the previous behaviour with



        runBf . parseBf


        However, speaking of parsing…



        Loop validation



        A drawback with our current BfProgram is that we might end up with mismatched brackets, e.g.



        parseBf "]["


        parses fine and leads to a runtime error. However, we could easily detect that during parsing. Our parseBf needs a way to report errors:



        type ParserError = String

        parseBf :: String -> Either ParserError BfProgram
        parseBf = go
        where
        go = Right
        go (x:xs) = case x of
        '<' -> MovLeft <$:> go xs
        '>' -> MovRight <$:> go xs
        '+' -> Increment <$:> go xs
        '-' -> Decrement <$:> go xs
        ',' -> Input <$:> go xs
        '.' -> Output <$:> go xs
        '[' -> -- exercise ; use `getLoop`-like function
        ']' -> -- exercise ; easier if previous one done correctly.
        x -> go xs
        x <$:> xs = fmap (x:) xs


        but afterwards, we can be sure that parseBf only returns BfPrograms with valid brackets.



        Unfortunately, we still need to use getLoop, as BeginLoop and EndLoop are still in our instruction set. If we change the instruction set, we can get rid of that too:



        data BfInstruction 
        = MovLeft
        | MovRight
        | Increment
        | Decrement
        | Output
        | Input
        | Loop BfProgram
        deriving (Show, Eq)


        I go into more details in some of my previous Bf reviews, feel free to read them if you get stuck on Loop.



        Final remarks



        Other than the usual re-evaluation of loops (which is a common scenario in Haskell Bf interpreters), your code was fine, so all the issues are really minor. Again: well done.






        share|improve this answer












        Well done. Overall, there are no big flaws, just some minor issues.



        Type annotations



        While it's great that all top-level functions have proper type signatures, the local bindings inside those functions usually don't. After all, their types should get inferred, e.g.



        example :: [Int] -> [Int]
        example = map inc
        where
        inc x = 1 + x


        doesn't need a type signature since x's type is already fixed to Int. It makes refactoring also a lot easier if we change the type later. If we started with



        example :: [Int] -> [Int]
        example = map inc
        where
        inc :: Int -> Int
        inc x = 1 + x


        and later want to generalize, we might forget the second type signature and end up with an error message:



        example :: Num a => [a] -> [a]
        example = map inc
        where
        inc :: Int -> Int -- whoops, GHC will yell about that
        inc x = 1 + x


        Therefore, type signatures for local functions are usually not written out. There are some instances where they're necessary, but that's usually with RankNTypes or other extensions.



        The tape



        The tape works well, and is pretty much how you would expect it.



        Infinite tapes and debugging



        That being said, an infinite tape has the slight inconvenience that you can never inspect it for debugging purposes.



        Also, if you ever create a module from your code, you must not export the Tape data constructor, as it would enable Tape 0 and therefore break assertions.



        A finite tape circumvents those issues, but needs slightly more effort in the movements.



        Working on the current value



        In runBf we can find several spots where we advance the program after we worked on the current value, e.g.:



                Increment -> advance prog (Tape ls (x+1) rs)
        Decrement -> advance prog (Tape ls (x-1) rs)


        That's now a possible source of errors, since we could have used



                Increment -> advance prog (Tape ls (x+1) ls)
        Decrement -> advance prog (Tape ls (x-1) ls)


        by accident. A small helper can prevent that issue:



        onCurrent :: (a -> a) -> Tape a -> Tape a
        onCurrent f (Tape ls x rs) = Tape ls (f x) rs

        current :: Tape a -> a
        current (Tape _ x _ ) = x


        Then we end up with



                MovLeft   -> advance prog (moveLeft tape)
        MovRight -> advance prog (moveRight tape)
        Increment -> advance prog (onCurrent (+1) tape)
        Decrement -> advance prog (onCurrent (subtract 1) tape)
        Input -> do
        char <- getChar
        advance prog (onCurrent (const (fromEnum char)) tape)


        Naming and scope



        As neither advance nor getLoop use any of the bindings in their scope, they're candidates for top-level functions.



        runBf' can be called go or another short name. Calling the inner worker just go is really common and won't alienate other readers.



        Make interfaces hard to use wrong



        getLoop uses an Int as first argument that's not properly documented. Types only go so far as documentation, and we could accidentally use getLoop 0 in BeginLoop.



        Instead, we should make it impossible to misuse getLoop:



        getLoop :: BfProgram -> BfProgram
        getLoop = go 1
        where
        go _ = error "Mismatched brackets in BF program"
        go 1 (EndLoop:ps) =
        go n (p:ps) = p:case p of
        BeginLoop -> go (n + 1) ps
        EndLoop -> go (n - 1) ps
        _ -> go n ps


        Similarly, runBf should probably take a BfProgram, not an arbitrary String, as this doesn't decrease the strength of your program, we can recreate the previous behaviour with



        runBf . parseBf


        However, speaking of parsing…



        Loop validation



        A drawback with our current BfProgram is that we might end up with mismatched brackets, e.g.



        parseBf "]["


        parses fine and leads to a runtime error. However, we could easily detect that during parsing. Our parseBf needs a way to report errors:



        type ParserError = String

        parseBf :: String -> Either ParserError BfProgram
        parseBf = go
        where
        go = Right
        go (x:xs) = case x of
        '<' -> MovLeft <$:> go xs
        '>' -> MovRight <$:> go xs
        '+' -> Increment <$:> go xs
        '-' -> Decrement <$:> go xs
        ',' -> Input <$:> go xs
        '.' -> Output <$:> go xs
        '[' -> -- exercise ; use `getLoop`-like function
        ']' -> -- exercise ; easier if previous one done correctly.
        x -> go xs
        x <$:> xs = fmap (x:) xs


        but afterwards, we can be sure that parseBf only returns BfPrograms with valid brackets.



        Unfortunately, we still need to use getLoop, as BeginLoop and EndLoop are still in our instruction set. If we change the instruction set, we can get rid of that too:



        data BfInstruction 
        = MovLeft
        | MovRight
        | Increment
        | Decrement
        | Output
        | Input
        | Loop BfProgram
        deriving (Show, Eq)


        I go into more details in some of my previous Bf reviews, feel free to read them if you get stuck on Loop.



        Final remarks



        Other than the usual re-evaluation of loops (which is a common scenario in Haskell Bf interpreters), your code was fine, so all the issues are really minor. Again: well done.







        share|improve this answer












        share|improve this answer



        share|improve this answer










        answered 13 mins ago









        Zeta

        15.1k23472




        15.1k23472






























            draft saved

            draft discarded




















































            Thanks for contributing an answer to Code Review Stack Exchange!


            • Please be sure to answer the question. Provide details and share your research!

            But avoid



            • Asking for help, clarification, or responding to other answers.

            • Making statements based on opinion; back them up with references or personal experience.


            Use MathJax to format equations. MathJax reference.


            To learn more, see our tips on writing great answers.





            Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


            Please pay close attention to the following guidance:


            • Please be sure to answer the question. Provide details and share your research!

            But avoid



            • Asking for help, clarification, or responding to other answers.

            • Making statements based on opinion; back them up with references or personal experience.


            To learn more, see our tips on writing great answers.




            draft saved


            draft discarded














            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f201746%2fa-simple-brainf-interpreter-in-haskell%23new-answer', 'question_page');
            }
            );

            Post as a guest















            Required, but never shown





















































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown

































            Required, but never shown














            Required, but never shown












            Required, but never shown







            Required, but never shown







            QYFZhvHi7,szg
            5q0zjtJJD6Px,rOmf4JYHdpGAq58nsTsRz

            Popular posts from this blog

            Scott Moir

            Souastre

            Biegi lekkoatletyczne