How to make your very own REPL in Haskell
This is a step-by-step tutorial on how to create a very simple REPL in Haskell. The goal of this walkthrough is to provide a basic understanding of REPLs and to show how easy and straightforward it can be to build one. We will create our REPL by individually constructing its four main components, and then composing them together.
Prior knowledge or experience with Haskell is not required to grasp the basic principles of this walkthrough, although some basic understanding of functional programming will probably make it a lot easier to understand the example code.
This tutorial has also been published as a blog post on open.bekk.no.
REPL stands for Read-Eval-Print-Loop, and as the name implies, its job is to:
- Read the given input, and parse it into a data structure that can be evaluated.
- Evaluate the parsed expression, based on a set of rules.
- Print the result of the evaluated expression.
- Loop (repeat) until signaled to stop.
REPLs are most commonly associated with programming languages, where they are used as an interactive shell for interpreting the code written in that language. Most languages often come with such REPLs already bundled into their toolkit (e.g. Ruby's IRB, or Haskell's GHCi). These days, there's also an ever-growing number of online REPLs that support several different languages, such as repl.it.
In these cases, the read and evaluate parts are focused around interpreting the input based on the language providing the REPL. However, a REPL doesn't necessarily have to be connected to a fully fleshed out programming language (or any language at all). Although that is the most common use case, one can argue that a REPL can pretty much be anything you want it to be, as long as it can read, evaluate and print whatever you throw at it in a repeating loop.
So by that definition, let's get started on creating our very own, very simple, REPL!
As stated earlier, a REPL consists of four steps. We will start by implementing each of these steps as separate functions and finally compose them together to create our REPL.
The first step is reading and parsing the input. Since we want to keep things simple, we'll ignore the parsing bit and stick to just reading the input as a regular string:
read' :: IO String
read' = getLine
For this, all we need is the getLine
function. This will read the input and return it as an IO String
. The IO
wrapper is there to indicate that the string was produced by an IO action (i.e. input/output), which in this case is some input provided by the user.
Note that the tick in the name of our read'
function has no special purpose other than to differentiate it from Haskell's built-in read
function.
Let's go ahead and add a few more lines here for convenience:
read' :: IO String
read' = putStr "REPL> "
>> hFlush stdout
>> getLine
We've added two additional actions here: putStr "REPL> "
simply prints REPL> at the start of the prompt, and hFlush stdout
is to immediately flush the stream just to make sure that nothing is stuck in the buffers. Finally we combine all three IO actions together with the >>
operator (which you can read as and-then).
The next step is to evaluate the input that we have read. The easiest implementation would be to just skip this part entirely, and let the function return its given input. So let's do that for now:
eval' :: String -> String
eval' input = input
No need for anything fancy here. The putStrLn
will print the given string to the console:
print' :: String -> IO ()
print' = putStrLn
The IO ()
type in the signature indicates that we are not returning anything, but still performing an IO action (i.e printing to the console).
The last step is to create a repeating loop around our three previous steps:
if (input == ":quit")
then return ()
else print' (eval' input) >> loop'
Here, we simply check if the input equals ":quit"
, in which case we exit our REPL. Otherwise, we evaluate the input,
print the result and restart the loop.
Finally, let's go ahead and put everything together inside our main
entry point:
main :: IO ()
main = do
input <- read'
unless (input == ":quit")
$ print' (eval' input) >> main
Firstly, the input string is extracted from the IO String
value coming from our read'
function, using the <-
operator. It's then passed on to the looping logic we defined earlier.
The unless
function here works exactly like our if
/else
logic in the previous code. It will exit the program if input == ":quit"
. The $
is the apply operator, which is used here to avoid wrapping the right side in extra parentheses.
So there we have it! A very simple, albeit rather useless REPL. You can find the complete code example in the Repl.hs file.
Compile the code by running ghc -o repl Repl.hs
. Then start, and test out the REPL by running ./repl
. It should look something like this:
$ ghc -o repl Repl.hs
[2 of 2] Compiling Main ( Repl.hs, Repl.o )
Linking repl ...
$ ./repl
REPL> Hello REPL
Hello REPL
REPL> 1,2,3,🍌
1,2,3,🍌
REPL> :quit
$
Due to the simple implementation of the eval'
function, the REPL just repeats whatever is typed.
The basic framework is however in place, and the type of evaluator that we place inside it will more or less determine the main purpose of the REPL.
There are a few examples of evaluators in the EvaluatorExamples.hs file. You can try them out by calling them from the eval'
function:
eval' :: String -> String
eval' input = simpleCalc input
This one turns our REPL into a very simple calculator. As we can see, the number of possibilities are endless when it comes to the types of REPLs you can create by simple changing the evaluator.
Going back to the main use cases of REPLs, you can even add in the evaluator of your own programming language here and create an interactive shell for your language. For that you might also want a custom parser in the read'
step though, so that the evaluator can work on a well defined data structure rather than a string.
The fully fleshed out REPLs that come bundled with programming language toolkits, usually have a considerable list of additional features and extra functionality. Even though these will not be covered in this tutorial, most of them are quite simple to implement and can easily be composed into this REPL just like what we did with the four initial steps. A few examples are:
- See a history of inputs and outputs.
- Set variables that can be accessed and used in later commands to the REPL.
- Special commands for debugging and error handling.
Take a moment to consider how these features could be incorporated into out current REPL.
There are many different tools and libraries out there for creating really powerful REPLs with lots of different features. One such example being parsec, a library containing a handful of useful functionality for reading and parsing input effectively.
The main purpose of this tutorial is however to provide a basic understanding of how REPLs work under the hood, and to show how simple it is to create and assemble the four main building blocks that constitute a Read-eval-print-loop.