in which static types are friends, not foes

I'm a big fan of composability in user interfaces. Unix has traditionally been strong in this area with its culture of pipes and standard in/out, making it easy to chain together small tools with orthogonal purposes. Unfortunately this usually does not extend to GUI tools which often tend towards monolithic globs of functionality. There are a few exceptions, my favourites including xbindkeys and mpd.

The most versatile of these I've found has been dmenu, a graphical option chooser in the style of Emacs' ido-mode: it presents a list of options, and as you type it narrows to just the options which match the input that's been entered so far. I've built a number of tools on top of this including a music frontend to mpd and a script that allows me to perform a number of Skype actions from the keyboard.

The problem is that dmenu is pretty minimalistic compared to ido. The main annoyance to me is that it doesn't support flex matching—in other words, only exact matches are shown. In Emacs, ido lets me match a few characters against the front of the string and a few against the tail. Any input is accepted as long as it uniquely identifies one of the choices. Since dmenu is written in C I didn't exactly relish the idea of picking up skills in that language to add this functionality, so I wrote a replacement in OCaml instead:

let rec read_lines lines =
  try read_lines (read_line () :: lines)
  with End_of_file -> lines

let lines = read_lines []

let lines_matching pattern matched line =
  try let _ = Str.search_forward pattern line 0 in 
      line :: matched
    with Not_found -> matched

let escape = function
  | ' ' -> ".*"
  | c -> Char.escaped c

let pattern input =
  Str.regexp (String.concat "" (List.map escape input))

let matched input lines =
  List.fold_left (lines_matching (pattern input)) [] lines

let rec draw_matches matches =
  Graphics.open_graph " 1440x15";
  Graphics.set_window_title "erythrina";
  Graphics.draw_string (String.concat " | " matches)

let finish input lines =
  Graphics.close_graph ();
  match matched input lines with
    | f :: _ -> print_string f
    | [] -> ()

let butlast input =
  match List.rev input with
    | [] -> []
    | _ :: rest -> List.rev rest

let rec main input =
  draw_matches (matched input lines);
  match Graphics.read_key ()  with
    | (* enter *) '\r' -> finish input lines
    | (* escape *) '\027' -> Graphics.close_graph ()
    | (* backspace *) '\b' -> main (butlast input)
    | (* any other *) c -> main (List.append input [c])

let _ = main []

In 39 lines of OCaml I've achieved near feature-parity to the 700 lines of C that make up dmenu. The only missing features are tab-completion and font/color customization, though I have added the flex-matching feature mentioned above that dmenu lacks, which makes tab completion less important.

This is my second foray back into the land of static typing since university. (Supposedly I learned C++ in school, but I've long since mercifully forgotten it all.) I picked up Mirah last year and spent a little more time hacking in it earlier this year for an Android app. While Mirah is a huge improvement over Java, its type inference unfortunately only extends to locals, (a common shortcoming of most JVM-hosted type systems from what I gather) meaning if you tend to write small methods you end up specifying all your types anyway. OCaml on the other hand infers all types using Hindley Milner inference, allowing the types to be effectively invisible.

I've only spent a few days writing OCaml, but I've got to say I'm impressed. The type system stayed out of the way, only making a fuss when I'd clearly made an error. Pattern matching is a dream: you'll notice that all the conditionals in the code above come from match rather than if. It's fast, it's pleasantly interactive, (especially via tuareg and Emacs) and the executables produced by the compiler are tiny and quick to start, which is valuable for anyone who spends a lot of time on the JVM and is looking for something to fill those pesky gaps for which the JVM is admittedly lousy.

My only real complaints surround the fact that the standard library is slim and quirky. It throws exceptions in places you wouldn't expect, like hitting the end of the file while reading or searching for a regex when the text contains no match. The regex support is a bit odd, but that's not too surprising considering how old the language is.

Of course the standard library is augmented by a number of third-party libraries. It seems like up until recently it's been assumed that you'll use either apt-get as the main way to pull these in or run make install from source. As I've blogged about recently, apt-get is a bit of a drag for libraries that are obscure or change frequently. It looks like Oasis is making it easier to build projects while ODB is the beginning of a rich library dependency system, though it's still got a long way to go. My needs have been simple enough that I've been able to stick with the standard library for my current project, but it's great to see progress in this direction.

Anyway, it's always a bit disorienting to toss yourself into a new environment like this. But OCaml has proven to be quite a treat. I recommend starting with this OCaml Tutorial which also links off to many other helpful resources. Have fun!

Update: As of version 4.5, dmenu has implemented fuzzy matching, making erythrina interesting mostly for educational purposes.

Update: I've posted further reflections on OCaml and how the ecosystem has changed in the time since this was written.

« older | 2011-08-15T01:37:00Z | newer »