Being dynamically typed doesn't mean that there are no types. And in fact, pretending that there are no types gives raise to a number of problems, like unclear semantics of a function and such.
Let's explore an example from the heart of Clojure.
The work horse in the very low-level code of the sequence
library is the lazy-seq macro. It returns an instance of
class clojure.lang.LazySeq which encapsulates the abstract
instructions to realise the actual sequence according to the
body of the lazy-seq macro.
Should this class implement the clojure.lang.ISeq interface?
To answer this question, we have to understand the types involved and what the semantics of the interface functions are.
First we have sequences—let's call the type “Seq”. A Seq is
either nothing – nil – or something with a first element and
a Seq of next elements. In particular there is not an empty
Seq! Clojure's interface to Seq is c.l.ISeq with the
functions first and next on the Clojure side.
Let's use some mathematically inspired notation.
first : Seq → Object
next : Seq → Seq
A Seq represents a sequential view on some collection of things.
That's nice, but how do we get a Seq in the first place?
Clojure's main factory for sequences is the – surprise! – seq
function. It may be used to construct a sequential view on an
instance of a given type. It works on Strings, Vectors,
Sets,… In particular is seq the identity on Seqs!
Let's call elements in the domain of seq Seqable.
seq : Seqable → Seq
This is not the end of the story, however. Why should only
physical things like Strings or Vectors be a source for
sequences?
Instead of providing the set of elements from which the view is
constructed, one could just as well provide some computation rule
to construct the sequence as it is needed. So one can eg. construct
a sequence of all natural numbers. Since these are infinite in size
they cannot be represented in a datastructure like a Vector.
Back in ye olden dayes this was done via lazy-cons.
lazy-cons : →Object x →Seq → Seq
lazy-cons constructed from a thunk which evaluates to an Object
(denoted as →Object) and a thunk which evaluates to a Seq
(denoted as →Seq) a sequence consisting of the Object as first
element and the result of the second thunk as next elements. The
evaluation of the thunks is only done when necessary. That is when
first or next are called on the result of lazy-cons.
So lazy-cons returns a true Seq. But there lies the rub. To
actually create a sequence via lazy-cons you already have to
know, that there is at least one element! The decision how it looks
like might be delayed, but that there is something, you have to
know.
This has implications. Consider the following example.
user=> (def x (drop-while odd? (iterate #(+ % 2) 1)))
Back when drop-while was implemented by virtue of lazy-cons,
this would send you in an infinite loop. Because you have to
know whether to return a lazy-cons or nil, you have to check
whether there is an even number in the input sequence or not.
To remedy this situation the interface of a sequence was modified.
Another function was added: rest. *In fact: rest was renamed
to next and rest got a new meaning.*
To be truely lazy, you have to also delay the decision of whether there are next elements.
The outcome might be that there are no more elements. However, you
don't know, yet. So you can't return nil. So, the return value
of rest cannot be a Seq anymore.
rest : Seq → Seqable
In fact, rest returns a Seqable, ie. you can call seq on
the return value to obtain again a Seq. This is also reflected
in the new lazy-seq constructor which replaces the old lazy-cons.
lazy-seq : →Seq → Seqable
So, although lazy-seq has “seq” in its name, it actually
doesn't return a sequence. And as such, I think it is a mistake
for c.l.LazySeq to implement c.l.ISeq.
Another example is the empty list (). The empty list is not a
sequence! If it were, seq would return () again, which it
does not. It returns (correctly) nil. Non-empty lists on the
other hand may serve as their own sequence. But this is merely
an implementation detail.
You are still awake? Wow! Congratulations.
This is a dry topic, but an important one. Even in a dynamically typed language like Clojure there are types. And it is important to understand their relationship. I think investing a bit of time in such considerations helps to get a deeper view into the problem domain.
Here the complete, consistent Seq interface in all its glory.
Although Clojure does not fully adhere to it.
first : Seq → Object
next : Seq → Seq
rest : Seq → Seqable
seq : Seqable → Seq
lazy-seq : →Seq → Seqable
; and formerly
lazy-cons : →Object x →Seq → Seq
Published by Meikel Brandmeyer on .
I'm a long-time Clojure user and the developer of several open source projects mostly involving Clojure. I try to actively contribute to the Clojure community.
My most active projects are at the moment VimClojure, Clojuresque and ClojureCheck.
Copyright © 2009-2014 All Right Reserved. Meikel Brandmeyer