Does Explicit vs Implicit and Fail Fast rule apply to Clojure when accessing nested maps/records?

Hi everybody,

Coming from Python I always remember a few rules:

  • Explicit is better than implicit.
  • Fail Fast, Fail Early

Which (I think) should apply to most of programming languages.

Other rule that applies to most of programming languages is that dot notation is preferred over square bracket notation. In other words

obj.foo;

is better over

obj["foo"];

and you should use the second (bracket) notation only when you have a property name stored in a variable (obj[var]) or there is some other special case (invalid chars, JSON…) or you assume that some external data might be wrong.

I spent last two weeks learning Clojure and I found that I can do:

(def testing-map
  {:foo 33
   :bar 44
   :foo-bar "foobar"
   :bar-foo {:another "something"
             :level {:this-is 383883
                     :the-last "my value"}}})

(:foo testing-map)
;; => 33

(:wrong-name testing-map)
;; => nil

(-> testing-map :bar-foo :level :the-last)
;; => "my value"

(-> testing-map :bar-foo :level :wrong-name)
;; => nil

(get-in testing-map [:bar-foo :level :the-last])
;; => "my value"

(get-in testing-map [:bar-foo :level :wrong-name])
;; => nil

All these examples are similar to bracket notation. I didn’t find anything dot notation -like that would give me an exception when a key in a map or a field in a record doesn’t exist.

I appreciate that I can use contains? function but that’s not the point. My point is that there is no easy way to get an exception when a key/field is not present and therefore I assume that Explicit vs Implicit and Fail Fast rule doesn’t apply to Clojure or at least not to dictionary-like data structures (map, record)

Do you agree?

Best regards,

Mark

You are perfectly right in your observation.
Getting a non existing element from a map returns nil.
Getting anything from nil return nil as well.

I am less sure, if this violates the principle of fail-fast, but this depends on the definition of “fail-fast”.
I think it is in-line with the principle of Clojure to be dynamically typed and functional.
Most mathematical functions don’t’ “fail”, if called with “unusual values”.
They calculate something (maybe useless)

So “get” of a map is defined as a function, which never fails and returns “nil” for most values.
And “get x of nil” is defined as return nil always, for any x.

4 Likes

An organizing principle of Clojure is: simplicity.

Mixing validation and program logic is contrary to simplicity. You wind up with logic that is obscured by validation conditionals, not to mention redundant. And you wind up not really knowing how completely your data has been validated.

In other words: the ideal would be to validate thoroughly, expressly, and separately (if you are going to validate at all); and let the logic be lightweight and pure.

Another organizing principle of Clojure is: openness.

get-in is a good example of openness. It is just one function of many. You may even write your own function, kind of like get-in but more restrictive. Your access to structure members is not limited by a certain syntax built into the language.

Another organizing principle of Clojure is: that it is fundamental. It is meant for making things with. Notation like the member.member.member notation focuses on “hard coding” or “hand coding” procedures. Clojure is higher-level. Clojure facilitates meta-programming.

3 Likes

There are some people who hate that aspect of Clojure, but I think the rationale for it can be illustrated in the Maybe Not talk.
nil in Clojure is unified with Java’s Null, but it has a semantic meaning, too: nothing, which is why it is inadvisable to put nils in your map.
You can read (get m k) as “what is mapped in m from k?”
If k is not in m the answer is nothing.
What is the first element in an empty sequence? nothing. (first []) => nil
What is mapped from nothing? nothing (get nil k) => nil
And so on.
Combined with nil-punning, i.e. nil is false-y, you can be very explicit yet forgiving:

(when-let [v (get-in m ks)] ,,,)

These behaviors compose well and there’s no need for anyone to fail.
We can ask the opposite question - why should map access or object field access fail?
Are you accessing uninitialized memory?

5 Likes

nil punning is a oft-underrated feature, and in the case of Clojure greatly reduces the times that you get a NPE compared to Java. You may pass in nil to many of the core functions and they will do the “correct” thing.

There are times when it is useful to access a field and know whether it found a value or not, which is why get, get-in and using keywords as functions accept an additional optional value to return if the key or key path isn’t found.

(:wrong-name testing-map :not-found)
;; => :not-found

(-> testing-map :bar-foo :level (:wrong-name :not-found))
;; => :not-found

(get-in testing-map [:bar-foo :level :wrong-name] :not-found)
;; => :not-found

This allows us to easily recover with a sane default value without wrapping our access inside of a try/catch. In my experience, the majority of cases where I want to access some value in a map and it’s not there are not “exceptional,” therefore it would be very troublesome if they threw an error and I had to wrap them in try/catches.

Rich Hickey’s design philosophy w.r.t. Clojure and its core library is nuanced, and talked at length about in the Maybe Not talk linked above. A fundamental idea in it is the open world assumption, that we may or may not know about certain things at different times and our programs need to be robust in the face of this. The rule fail fast, fail early in the case of accessing a missing piece of data is in conflict with this other rule of being robust in the face of missing information, and the decision was made to opt for being robust.

8 Likes

What other langs besides python (and js?) have this? In C-like languages those constructs have nothing to do with each other, and in ML-derived ones, I think they don’t even exist…

1 Like

I think Rust has a similar thing, at least in async Rust future-chaining results in fail-fast behavior (with sane error propagation):

my_fut.await?.calculate_stuff().await?...
1 Like

OUUUHH! I didn’t know that. Thx!

That’s a good point, C# won’t let me compile with wrong property name.

I think missing key is more like a missing “x” in a mathematical function. For example <not here> + 1 wouldn’t make sense therefore it would fail.

Is this a logic or just letting the programmer know that he/she made a mistake and that “fooobar” should be “foobar” instead of thinking that it has nil value?

That means that (get-in testing-map [:bar-foo :level :the-last]) is not good either since in my opinion that is the same as member.member.member, just a different syntax.

In most cases because I made a mistake. I guess that answer is more testing.

I get your point everyone ;-).

I need to get used to that and hopefully one day it’ll feel normal.

1 Like

It probably won’t let you compile if you use array notation for properties, either…

Most mathematical functions don’t’ “fail”, if called with “unusual values”.
They calculate something (maybe useless)

To be fair, mathematical functions do exactly that (fail) – they are well-defined over and cannot be applied (the result simply does not exist) outside their domain; an attempt of such application would correspond to an exception or undefined behaviour with their interpreter – even clojure shouldn’t let us divide by zero :).

Mathematically speaking, clojure implicitly extends its maps to be total functions on the set of all possible in-language values while python does not. Neither is somehow incorrect, but python’s {0: 1} would correspond to mathematical function {(0, 1)}, which I think is intuitively closer than the extended definition one would get with clojure’s operations.

1 Like

Clojure pushes you to model data using maps, which are open and dynamic. So it’s often assumed a map with an optional key might be missing the key, but that’s normal. Some people in the community have been calling this style data-oriented programming.

(def user1 {:first-name "John"})
(def user2 {:first-name "Bob" :last-name "Dylan"})

(get user1 :last-name)
nil

(get user2 :last-name)
"Dylan"

In OO languages, you’d have a defined schema, normally a class, which specifies which fields are expected to be there. So if you use the object and try to get a key not defined on the schema (the class), it makes sense to error.

With a schema, you’d say the key must be present, but the value can be missing (by making it null). So if the key is missing error, but if the value is missing return null.

In Clojure the key doesn’t have to be present, so throwing an error would be strange.

So what people tend to do in Clojure is that the key can be missing (by not having en entry for it), but the value must be present. Nothing enforces this, but that’s often how I spec my entities and validate them.

For example, I’d consider this wrong:

(def user1 {:first-name "John" :last-name nil})

If the last-name is optional, in the case where it was not provided I’d make sure to model the map so that the key for it is also removed from the map:

(def user1 {:first-name "John"})