I’ve heard a lot of things about Racket (well really, things about many different Lisp dialects), and most of them were good. Recently, I decided to try to decide between Haskell and a Lisp once and for all. I wanted to go for a Lisp–1, since they keep functions and values in the same namespace, which is how it should be. Eventually, after
trying out Guile, Chicken Scheme, and a few others, I settled on Racket.
Setting Racket up
This was pretty easy. Unlike with Guile, OpenBSD has a very up-to-date Racket package, so I just installed that.
# pkg_add racket
Now I had a Racket interpreter living at /usr/local/bin/racket. Sure, I could have just run that and started to play around, but I really wanted to install a mode for Emacs, which is what all serious Lisp users do. A quick Google revealed geiser-mode as the premier Scheme interaction mode. Opening up Emacs, I installed that with a quick “M-x package-install RET geiser-mode RET”, and that was that.
There were a few bindings I had to get used to, but the real interesting part of my experience was using Racket, not my editor (although geiser-mode is pretty good).
After playing around for a bit and installing paredit-mode, I was ready to get started.
The first thing you notice when using a Lisp is the large number of parentheses. In Racket, sometimes square brackets are used to clear things up, which really helps. I’ve never seen this in Common Lisp or other Schemes, so this must be something Racket-specific.
(let ((a 5) (b 10)) (+ a b))
That seems pretty trivial and non-threatening, right? Right? Well, it doesn’t stay very nice for long unless you use square brackets or some other distinct type of bracket.
(let ((a (map (lambda (x) (equal? x "hello")))) (b (get-field info some-struct))) (map list a b))
Maybe you don’t think that’s too bad, but it looks a lot better with square brackets. Mainly, it lets you keep track of which parens close with expressions. Even with rainbow-delimiters mode, it’s really difficult! It’s a bit less difficult in Racket, though.
Coming from Haskell, it was a bit jarring to see that there was no type safety at all in default Racket. You could have something like this:
(if (equal? (read-line) "error out please") (read-line 4 5) (+ 3 4))
Of course, since
(read-line) doesn’t take any arguments, you’ll get an error at runtime complaining about giving it the wrong number of arguments. But that’s the point, you only get the error at runtime. In fact, if you never type in “error out please” you’ll never get an error. Oh man, that’s scary: you’re supposed to catch all errors at compile-time with a good type system.
In Racket, you start every file with a declaration of what language you’re using. Usually:
But sometimes you might be writing a document or info file for raco, so you’ll want a different one. Here’s one I always use, especially after that horror story about runtime errors:
Pretty self-explanatory, I think. Here’s how some normal Racket code might look:
(define (square n) (* n n)) (define my-numbers (list 1 2 5 3 8 5))
In Typed Racket, you can put in type annotations, too! It’s usually pretty easy to see what’s meant. Here are the appropriate type annotations for the two bindings above:
(: square (-> Number Number)) (: my-numbers (Listof Integer))
Those actually do look similar to type annotations in Haskell:
square :: (Num a) => a -> a myNumbers :: [Int]
Personally, I like the Haskell syntax more. Of course, since Racket is a Lisp, the syntax can be changed infinitely, so that’s not really a complaint.
All in all, the Racket REPL is cool (although nothing special nowadays), the language is good if unsafe, Typed Racket is nicer, Haskell, ML, OCaml and other “real” functional languages still feel nicer to me, though.
A few days isn’t enough to really get to know a language, though, so maybe I’m wrong. Until someone tells me so, I won’t use any Lisp as extensively as I do Haskell, mostly because it feels unsafe.