VocalBit« assorted thoughts on technology, computers and programming »

Exploring Type Classes in Nimrod

Nimrod's type classes appear simple, flexible, powerful and far superior to interfaces in their design.

The up-and-coming Nimrod programming language recently added an interesting type classes feature. Type classes are used to specify an arbitrary set of requirements that a type must satisfy. What makes Nimrod's implementation interesting is how these requirements are specified.

Defining Type Classes

Here is a how you define type classes (with tidbits about Nimrod along the way):

type
  Paper = object
    name: string

  Bendable = generic x
    bend(x)

proc bend(p: Paper): Paper = Paper(name: "bent-" & p.name)

var p = Paper(name: "red")
echo p is Bendable  # prints 'true'
echo 42 is Bendable # prints 'false'

Above, Paper is a typical object definition - Paper objects contain a field name of type String. The proc bend line defines a function called bend, that accepts and returns an object of type Paper. Nothing fancy, yet.

The generic keyword creates a type class called Bendable. Here x is a parameter used in the indented body to specify requirements of the Bendable type class. Essentially, x represents any object whose type satisfies the requirements of Bendable. The indented body specifies what must be doable with x. In the example above, it basically says you must be able to call a function ``bend(x)``.

The interesting thing is that the body contains arbitrary Nimrod code. If the body passes compilation checks, then type(x) satisfies the type class requirements, else it doesn't. Note that this code is never actually executed. This gives you considerable freedom to define the requirements on a type. For instance, in the example above, we don't specify the return type of bend(x) (we could if we wanted).

To make things easier within the type class body, the following rules also apply:

  • You can write a type name T when you mean a value of type ``T``.
  • If you write a compile time predicate (e.g. s is String), it must evaluate to true for the check to pass.

Putting it together, we might describe a FileLike type class as follows:

type FileLike = generic f
    # reading f returns a string
    var data: string = read(f)

    # writing returns number of bytes written
    var written: int = write(f, String)

    # iteration yields strings
    for line in f:
        line is String

    close(f)

I find such a definition extremely readable and also easy to write - much more so than the clunky interfaces found in some languages. I don't have to remember special function names or keywords for iterator definitions, I simply write out how I want the objects to be used! As a bonus, I might also have a nice example to show others how to use a type.

Using Type Classes

You use type classes in places where you would otherwise specify types. As function parameter types, e.g.:

proc doubleBend(b: Bendable): type(b) =
    return bend(bend(b))

doubleBend(Paper("green")) # prints 'bent-bent-green'
doubleBend(123) # compilation error because bend(:int) not defined
doubleBend("abc") # compilation error because bend(:string) not defined

Any object that satisfies the type class can be passed into the function parameter requiring the type class.

The example above follows from the earlier definition of Paper. Note that nowhere we explicitly state that Paper satisfies the Bendable type class. The type class requirements are automatically checked when you call doubleBend() with a Paper object.

You could also use type classes to parameterize types, e.g.:

type Rope = object
    length: int

proc bend(r: Rope): Rope = Rope(length: r.length / 2)

# seq[T] is a built in parameterized type
# it is a dynamically sized array containing objects of type T

proc bendEm(bendy_list: seq[Bendable]):
    for bendy_object in bendy_list:
        bend(bendy_object)

var ropes = @[Rope(10), Rope(15)]
var papers = @[Paper("red"), Paper("green"), Paper("blue")]

bendEm(ropes)
bendEm(papers)

Observe again that we don't need to explicitly state that Rope satisfies Bendable. Type classes give us convenient and powerful ad-hoc polymorphism.

More Information

The type class feature is still being polished and while the basic examples should work, the advanced ones may not work yet.

For more information, see: