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!