Extend IFn protocol like if you were part of clojurescript core team

In Clojure, the language itself is modifiable

In clojure , the developer and the author of the language have a very intimate relationship: almost every part of the language author is extensible and modifiable by the clojure developer.

It is well known that macros allow the developer to add new semantics to the language - as if they were an original part of the language.

But what about functions?

Functions as a concept is at the core of clojure (like for any other functional programming language). So it’s not obvious to imagine that any kind of extensibility will be provided also for the concept of function.

I felt completely enlightened when I discovered - after a couple of months of clojure programming - that:

In clojurescript, any type of the language could behave like a function

This article will guide you through the path to enlightenment…





Keywords and maps as functions: how does it work?

You probably already know that keywords and maps could be used as functions.

Regular keyword get:

(:a {:a "A"})

keyword get - with not-found values like get:

(:a {} :n/a)

regular map get:

({:a "A"} :a)

map get - with not-found values like get:

({} :a :n/a)

Now, let’s see what happens behind the scenes…

We will see that the mechanism that enables keywords and maps to behave like function is completely open and extensible.

Let’s have a look at an excerpt from clojurescript source code to understand this mechanism.

Don’t be afraid by the big pyramid…

( defprotocol IFn "Protocol for adding the ability to invoke an object as a function. For example, a vector can also be used to look up a value: ([1 2 3 4] 1) => 2" ( -invoke [ this ] [ this a ] [ this a b ] [ this a b c ] [ this a b c d ] [ this a b c d e ] [ this a b c d e f ] [ this a b c d e f g ] [ this a b c d e f g h ] [ this a b c d e f g h i ] [ this a b c d e f g h i j ] [ this a b c d e f g h i j k ] [ this a b c d e f g h i j k l ] [ this a b c d e f g h i j k l m ] [ this a b c d e f g h i j k l m n ] [ this a b c d e f g h i j k l m n o ] [ this a b c d e f g h i j k l m n o p ] [ this a b c d e f g h i j k l m n o p q ] [ this a b c d e f g h i j k l m n o p q r ] [ this a b c d e f g h i j k l m n o p q r s ] [ this a b c d e f g h i j k l m n o p q r s t ] [ this a b c d e f g h i j k l m n o p q r s t rest ])) ( deftype Keyword [ ... ] ... IFn ( -invoke [ kw coll ] ( get coll kw )) ( -invoke [ kw coll not-found ] ( get coll kw not-found )) ... ) ( deftype PersistentHashMap [ ... ] ... IFn ( -invoke [ coll k ] ( -lookup coll k )) ( -invoke [ coll k not-found ] ( -lookup coll k not-found )) ... )

What an amazing discovery:

clojurescript exposes its most basic element - the function - as a protocol!!!

Keywords and maps behave like functions simply because they implement the -invoke method of IFn protocol.

And you - as a clojurescript developer - are not limited to the types that implement the IFn protocol. The reason is that in clojurescript every protocol can be extended through extend-type and extend-protocol after the type is defined.

We will see now how simple it is to make strings behave like functions.

From this below, we are dealing with hacky stuff. The objective of this article is to illustrate advanced feature of the language and I definitely discourage you to deal with that hacky stuff on production code.

David Nolen stated it this way in a interesting discussion with me on clojurians Slack:

“Extending base types is really about convenience, expressivity - no way at the moment for it to be performant” - David Nolen.

Strings as functions: Home brew

Let’s see how to make strings behave like functions using extend-type and implementing -invoke in a similar way that it was done for keywords in clojurescript source code:

(ns my.playground) (extend-type js/String IFn (-invoke ([s coll] (get coll (str s))) ([s coll not-found] (get coll (str s) not-found)))) ("a" {"a" "A"})

And it works also with not-found values like get:

("a" {} :n/a)

For some technical reason (see CLJS-1618), we have to wrap the string into str . But beside that, it is really simple.

Also, this extensibility for base types is not available in clojure . The reason is that in clojure only types that are defined in clojure can be extended to java interfaces. And IFn is a java interface and the base types (strings, numbers, regexps, maps,etc..) are defined in java .

Regular expressions as functions: Home brew

Now, let’s see how to do fancy stuff with regular expressions.

Instead of having to write this:

(re-find re s) to check if a string matches a regular expression (clojure.string/replace s match replacement) to do string replacements

We will show what to do in order to be able to write that:

(re s) to check if a string matches a regular expression (match replacement s) to do string replacements

Let’s see it in action with KLIPSE:

(ns my.regexp (:require [clojure.string :as string])) (extend-type js/RegExp IFn (-invoke ([match s] (re-find match s)) ([match replacement s] (string/replace s match replacement)))) (#"clojure" "clojurescript")

And now of the string replacement:

(#"clojure" "clojurescript" "clojure rocks")

Clojurescript extensibility allowed us to create new type of functions with a few lines of (simple) code…

Don’t you feel enlightened?

Like I said above, dealing with base types is really dangerous. So, please be careful…

If you have any ideas about implementing IFn protocol for custom types, I’d really appreciate if you share it in the comments below.

Clojurescript rocks!