Implementing yes(1) in Racket
As an in-retrospect-inevitable consequence of my recent post about implementing yes(1)
in Haskell, I found myself itching to try out the implementation in a whole bunch of my less-familiar languages.
A language that I’ve been slowly picking up lately is Racket, a Lisp (based on Scheme) designed for designing and implementing programming languages.1
It’s the first Lisp I’ve properly learned (after being scared away from Common Lisp), and it’s fun to play around with — the syntax makes for a pleasant break from typical C-family language syntax.
A break was just what I needed,2 so let’s implement yes(1)
in Racket!
I’m going to be writing this as an interpreted script rather than a compiled program, so let’s start with the hashbang line:
#!/usr/bin/env racket
One of Racket’s most interesting and unique features is its #lang
directive, which lets you write code in any number of languages (which are then translated to Racket code by interpreters behind the scenes).
There are a handful of built-in languages.
For the most part, Racketeers try to stick to something like racket/base
, a subset of the full Racket environment, but we’re going to need some things not offered there, so let’s just go with #lang racket
.
#lang racket
Let’s start by defining a function that retrieves the target string to be printed from a list of command line arguments.
(define (fetch-repeat-string args)
(if (empty? args) "y" (string-join args " "))
Hopefully this is easy enough to read: if the argument list given is empty, default to the string "y"
, else join the list with spaces and use that.
Let’s take this a step further.
One of the other interesting features of Racket is contracts, which allow a way of enforcing many behaviors at runtime.
There’s a lot more to contracts that I won’t get into here, but for the purposes of this I just want to use them as a sort of type system.
To define a contract at the function boundary, we can use the convenient define/contract
form.
(define/contract (fetch-repeat-string args)
(-> (listof string?) string?)
(if (empty? args) "y" (string-join args " ")))
The contract in the second line can be read as “this function takes a list of strings and returns a string.”
This is something like &[String] -> String
in Rust or [String] -> String
in Haskell, but it’s enforced at runtime here.
If the contract is violated, we get a nice error message saying what went wrong and who’s to blame.
Now, we want to be able to fetch a list of command-line arguments in order to determine the string to print.
Racket’s docs discuss in great detail the command-line
form, but since we don’t need anything too fancy here, let’s just use the current-command-line-arguments
form.
(println (fetch-repeat-string (current-command-line-arguments)))
Let’s give it a shot.
$ chmod +x yes.rkt
$ ./yes.rkt
fetch-repeat-string: contract violation
expected: list?
given: '#()
in: the 1st argument of
(-> (listof string?) string?)
contract from:
(function fetch-repeat-string)
# ...
Huh?
It looks like we’re not getting a list from current-command-line-arguments
.
Let’s review the docs again:
(current-command-line-arguments)
→ (vectorof (and/c string? immutable?))
Ah!
current-command-line-arguments
gives us a vector, not a list.
Interesting.
Well, let’s update our contract to take a vector instead as well.
(define/contract (fetch-repeat-string args)
(-> (vectorof string?) string?)
(if (empty? args) "y" (string-join args " ")))
And now let’s try again.
$ ./yes.rkt
string-join: contract violation
expected: (listof string?)
given: '#()
# ...
Ah, so string-join
wants a list.
Well, let’s give it a list, then.
We can readily convert a vector into a list using the vector->list
form.3
First, let’s change our contract back.
(define/contract (fetch-repeat-string args)
(-> (listof string?) string?)
(if (empty? args) "y" (string-join args " ")))
And now let’s change the other part:
(println
(fetch-repeat-string
(vector->list
(current-command-line-arguments))))
Surely this ought to work?
$ ./yes.rkt
"y"
The first thing I notice is that the quotation marks indicating that it’s a string are still present.
We can fix that readily enough by using displayln
instead of println
:
(displayln
(fetch-repeat-string
(vector->list
(current-command-line-arguments))))
Let’s check to make sure.
$ ./yes.rkt
y
Cool! Now we just need to write a function to print the string repeatedly. To do this, we can just make a recursive function:
(define (print-forever str)
(define (loop)
(displayln str)
(loop))
(loop)
The print-forever
function defines a new function (conceptually, a closure capturing the value of str
) which calls itself repeatedly, then calls this function once to kick off the chain of execution.
Let’s enhance it with a contract.
Since this is a divergent function, it doesn’t return anything of meaning, so let’s say it returns #<void>
.
(define/contract (print-forever str)
(-> string? void?)
(define (loop)
(displayln str)
(loop))
(loop))
Now let’s update the tail end of the script and try it out.
(begin
(define str
(fetch-repeat-string
(vector->list (current-command-line-arguments))))
(print-forever str))
Here we go!
$ ./yes.rkt
y
y
y
y
# ^C
$ ./yes.rkt "hello world"
hello world
hello world
hello world
hello world
# ^C
$ ./yes.rkt hello world
hello world
hello world
hello world
hello world
# ^C
It looks like we’ve implemented (most of) yes(1)
in Racket!
The final source listing is below.
; yes.rkt
#!/usr/bin/env racket
#lang racket
(define/contract (fetch-repeat-string args)
(-> (listof string?) string?)
(if (empty? args) "y" (string-join args " ")))
(define/contract (print-forever str)
(-> string? void?)
(define (loop)
(displayln str)
(loop))
(loop))
(begin
(define str
(fetch-repeat-string
(vector->list (current-command-line-arguments))))
(print-forever str))
-
To those interested in using Racket to implement programming languages, I highly recommend Matthew Butterick’s Beautiful Racket. ↩
-
And a good friend nerd-sniped me. Again. ↩
-
One of my favorite parts of Racket is the amount of extra expressivity granted by the ability to use characters like dashes and
<
/>
in identifiers.vector->list
is so clearly a conversion and it’s nice to look at, with just a bit of added whimsy. ↩