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.
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:
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.
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.
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: