Writing a simple REPL in Go

Go is a great language for writing tooling. It's very easy to produce a self-contained binary in go which makes very easy to distributed tools written in go. Today I want to show how easy is to write a simple REPL in Go. REPLs are quite popular in Functional Programming community like Scala, Clojure, and Haskell. Today even Java has a REPL :-). Cassandra has interesting REPL called CQLSH which allow local and remote connections. Currently, I'm thinking to write REPLs for some things I´m working with. REPLs are great for productivity and context. No to mention the could reduce and simplify discovery and troubleshooting. You might be thinking Gosh to write a REPL is tough we need to have parsers, print things nicely with colors, use tab completion, remember all previous commands. and one and on... I'm a full fledge REPL might require more work however a basic and simple version is something we can do in less them 5 minutes so I won't be doing any complicated parser or parsing combinators here, however, I will show how easy we can hook simple commands and expressions in a smooth command line experience. This is not a production ready REPL but some simple ideas I want to share.  Go has some nice APIs to deal with IO, for Simple Math and Boolean Expressions I will be using Govaluate. The only dependency(besides the core language packages) we will use will be Govaluate. So, in order to install Govaluate, we need to run in the console: $ go get github.com/Knetic/govaluate I'm only using it here because I show how to do math expressions but depending what you are doing you really won't need it - Feel free to drop it and have a light binary in the end :-)


Down to the Code

Let's take a look on the code - main.go


Basically, we will write our code in a loop. We will keep waiting for commands until the user type "exit". Go does not have a while or do-while commands so we need use for. Go can be a bit verbose sometimes(Nothing compared with Java) but a trick that works for me is creating lots of functions.  With functions, we can HIDE some details and also end up having a code that is easier to READ. I will use a Map where the key will be a String and the value will be an Interface{}. So don't have proper generics so when we need do things in a generic way we need to use Interface[]. So this map will map String alias to functions. Functions will be actual command implementations. I defined my functions before the main function so when you see a function as a value on the command maps you now function is defined outside of the map. 

The code will check if what the user type on REPL contains on the map, so let's say the user type "cls" which is the command to clear the terminal screen. This key will be on the map so we will get Interface{} back, now we need the call the function which is held as Interface{} so the syntax is a bit strange but is something like: variable.(func())() So here we are CASTING the Interface{} to a function which no arguments and we are calling the function. 

The code makes OS interaction easy we only need to do exec.Command("my Linux command") and that's it we are executing OS level commands and scripts. This happens inside the cls function. In case that we cant find the function on the Map we go to a callback function. So the fallback function will ignore the ENTER key by replacing "\n" on the string. So, even so, we still have one more fallback so it might be a simple math expression so I will try to evaluate the text with Govaluate however it could panic. In order to make the program safe we need use DEFER and RECOVER. Which is the right way to handle PANIC in go. Defer will make the function be pushed to the end of the stack and recover will only return data in case of PANIC.

Here we have some basic functions like help, cls and time. Help just print what are the available functions with some hints and basic documentation. Time uses time. Now in order to get current Date / Time in Go. Exist is not a function but a condition to break the for loop that you can see on the function shouldContinue. you might realize the code in full of underlines "_" often used when you want to ignore some return value. Go don't have exceptions - so you need return and deal with errors on every function call. Sometimes you don't handle the error or you can handle it later so you can use "_" to ignore the variable. As you might realize go allow you to return multiple values in functions.

If you want you can run our REPL doing $ go run main.go or if you want to create a single binary you just type $ go build on the same folder as you have the main.go file. So now we have a simple REPL working with less tham 100 lines of code.

Cheers,
Diego Pacheco







Popular posts from this blog

Kafka Streams with Java 15

Rust and Java Interoperability

HMAC in Java