Chapter 40. Literal Map

Represent an expression as a literal map.

image

40.1 How It Works

A Literal Map is a language construct, present in many languages, that allows you to form a map data structure (also known as a dictionary, hashmap, hash, or associative array). It’s normally used in a function call where the function takes the map and processes it.

The biggest problem with using a Literal Maps in a dynamically typed language is the lack of a way to communicate and enforce the valid names for the keys. In addition to the fact that you’ll have to write code to handle unfamiliar keys yourself, there’s no mechanism to indicate to the DSL script writer which keys are correct. A static language allows to avoid this problem by defining enums of a particular type to be the keys.

In a dynamically typed language, the keys of a Literal Map are usually a symbol data type (or, failing that, a string). Symbols are the natural choice and are easy to process; some languages offer syntax shortcuts to make it easy to use symbol keys, as they are so common. Ruby, for example, can replace {:cores => 2} with {cores: 2} as of version 1.9.

Just as I treat a varargs function call as a form of Literal List, I treat a function call with keyword arguments as a form of Literal Map. Indeed, keyword arguments are even better, as they often allow you to define valid keywords. Sadly, keyword arguments are even rarer than a literal map syntax.

If you have a literal list syntax but not a literal map, you can use literal lists to represent maps—this is what Lisp does with an expression like (processor (cores 2) (type i386)). In other languages, you can achieve that by using a construct similar to processor("cores", 2, "type" "i386"), treating arguments as alternating keys and values.

Some languages, Ruby for example, allow you to omit the delimiters for a literal map when you are only using one of them within a particular context. So, instead of writing processor({:cores => 2, :type => :i386}), you can shorten it to processor(:cores => 2, :type => :i386).

40.2 When to Use It

Literal Map is a great choice when you need a list of different elements where each element should appear no more than once. The common lack of key validation is annoying but, overall, the syntax is usually the best choice for this case. There’s a clear communication that each subelement can only appear at most once, and the map data structure is ideal for the called function to process.

If you don’t have Literal Maps, you can make do with a Literal List, or use Nested Functions or Method Chaining.

40.3 The Computer Configuration Using Lists and Maps (Ruby)

Following its scripting language tradition, Ruby provides very good literal syntax for lists and maps. Here’s how we can use this syntax for the computer configuration example:

image

I’m not using just Literal Map here; as usual, it’s good to mix Literal Map with other techniques. Here I have three functions: computer, processor, and disk. Each of these functions takes a collection as an argument: computer takes a Literal List, the others take a Literal Map. I’m using Object Scoping with a builder class that implements the functions. Since this is Ruby, I can use instance_eval to evaluate the DSL script in the context of an instance of the builder, which saves me from having to make a subclass.

I’ll start with processor.

image

Making use of a Literal Map is simple; I just pick the necessary items out of the map using the keys. The danger with using a map like this is that it’s easy for the caller to introduce an incorrect key by accident, so it’s worth a little checking here.

image

I use the same approach for the disk.

image

Since everything is a simple value, I can create the domain object and return it within each Nested Function. The computer function can create the computer object, using a vararg for the multiple disks.

image

(Using a “*” in the argument list enables variable arguments in Ruby; in the argument list, *disks indicates a vararg. I can then refer to all the disks passed in as an array named disks. If I call another function with *disks, the elements of the disks array are passed in as separate arguments.)

To process the DSL script, I get the builder to evaluate the script using instance_eval.

image

40.4 Evolving to Greenspun Form (Ruby)

As with other elements in an internal DSL, a good DSL uses several different techniques together. So in the previous example, I used Nested Function and Literal List as well as Literal Map. Sometimes, however, it’s interesting to push a single technique as far as it can go just to get a sense of its capabilities. It’s quite possible to write even a fairly complex DSL expression using only Literal List and Literal Map. Let’s see what that might look like:

image

In this version, I’ve replaced all the function calls with Literal Lists, where the first element in the list is the name of the item to be processed and the rest of the list contains the arguments. I can process this array by first evaluating the Ruby code and then passing it to a method that interprets the computer expression.

image

I handle each expression by checking the first element of the array and then processing the other elements.

image

This essentially follows the form of a Recursive Descent Parser. I say that the computer clause has a processor and multiple disks, and I call the methods to process them, returning a newly created computer.

Handling a processor is straightforward—just unpick the arguments out of the provided map.

image

Handling the disks works the same way.

image

One thing to notice about this approach is that it gives me complete control over the order of evaluation of the elements of the language. I choose here to evaluate the processor and disk expressions before creating the computer object, but I can do things pretty much any way I wish. In many ways, this DSL script is like an external DSL encoded in internal literal collection syntax instead of a string.

This form mixes lists and maps, but it’s also possible to do this using only Literal List, which might appropriately be called Greenspun form.

image

(I’ll leave it as an exercise for the reader to determine why I call this piece of programming whimsy “Greenspun form.”)

All I’ve really done here is replace each map with a list of two-element sublists, where each sublist is a key and value.

The main loading code is the same, breaking down the symbolic expression (sexp) for the computer into a processor and several disks.

image

The difference comes with the subclauses, which need some extra code as an equivalent of looking up things in a map.

image

Using only lists does result in a more regular DSL script, but using a list of pairs as a map doesn’t fit in so well with Ruby’s style. Either case isn’t as good as the earlier example which mixed function calls with literal collections.

Yet this approach of nested lists does lead us to another place where this style is natural. As many readers will have long ago realized, this is essentially what Lisp looks like. In Lisp, the DSL script might look like this:

image

The list structure is a lot clearer in Lisp. Bare words are symbols by default, and since expressions are either atoms or lists, there’s no need for commas.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.69.199