My daily language is Clojure. One of the joys of working in Clojure is its great core library. The core library has a wealth of functions that apply broadly across data structures. A typical function looks like this:
(defn nthnext
"Returns the nth next of coll, (seq coll) when n is 0."
{:added "1.0"
:static true}
[coll n]
(loop [n n xs (seq coll)]
(if (and xs (pos? n))
(recur (dec n) (next xs))
xs)))
I want to call your attention to two specific forms. The “seq” function works on any “Seqable” collection type. (N.B.: It has special cases for other types, including some to make Java interop more pleasant. But the core behavior is about Seqable.) The “next” function is similar: it works on anything that already is a Seq or anything that can be made into a Seq.
This provides a nice degree of abstraction and through that, generality.
Pretty much all of the core data types either implement ISeq or Seqable. That means I can call “seq”, “next”, and “nth” on any of them. Other data types can be brought into the fold by extending one of those interfaces to them. We extend the data to meet the core functions, instead of overloading functions for data types.
YAGNI Isn’t About Being Specific
Under this approach, writing a general function is both simpler and easier than writing a specific one.
For example, suppose I need to do that classic example of trivial functionality: summing a list of integers. The most natural way for me to write that is like this:
(reduce + 0 xs)
That is both simple and general. But it doesn’t meet the spec I said! It sums any numeric type, not just integers. If I decide that I really must restrict it to integers, I have to add code.
(assert (every? integer? xs))
(reduce + 0 xs)
This is a pattern I find pretty often when working in Clojure. When I generalize, I do it by removing special cases. This goes hand-in-hand with decomposing behavior into smaller and smaller units. As each unit gets smaller, I find it can be more general.
Here’s a less trivial example. Today, I’m working on a library we call Vase. (See Paul deGrandis' talk on data-driven systems for more about Vase.) In particular, I’m updating it to work with a new routing syntax in Pedestal. With the new routing syntax, we can build routes from ordinary Clojure data–no more need for oddly-placed syntax-quoting.
One of the core concepts in Pedestal is the “interceptor”. They fulfill the same role as middleware in Ring. (One difference: interceptors are data structures that contain functions. Interceptors compose by making a vector of data, whereas Ring middleware composes by creating function closures. I find it easier to debug a stack of data than a stack of opaque closures.) Any particular route in Pedestal will have a list of interceptors that apply to that route.
When a service that uses Pedestal supplies interceptors, it composes a list of them. Suppose I want to make a convenience function that helps application developers build up that list. What would I need to do?
You probably already figured out that any such “convenience” functions I could create would basically duplicate core functions, but with added restrictions. Instead of “cons”, “conj”, “take”, and “drop”, I’d have to create “icons”, “iconj”, “itake”, and “idrop”. What a waste.
I have to ask myself, “Do I need some special behavior here?” And the answer is “YAGNI.”
YAGNI Is About Adding “Stuff”
YAGNI is commonly understood to mean “don’t generalize until you need to.” In some languages and libraries, I suppose that’s the right read. In my world, though, it is specializing that requires adding stuff. So I often call YAGNI if someone tries to make a thing less general than it could be.
Small functions that operate on abstractions instead of concrete types are both general and simple.