I think there’s a terminology mismatch.
Encapsulation the way I see it from the quote talks about bundling of methods and data together and the ability to protect state from erroneous modifications that could break other users of the shared state.
You seem to be broadening this definition to also mean hiding of implementation details, and I don’t think the book is talking about hiding implementation details.
What that means is that yes you can still hide implementation details in Clojure, and you don’t need to bundle methods and data together to do it (as I demonstrate further down)
If you stick to the narrower more specific definition, the book is correct, immutability is a replacement for protecting data invariants on shared mutable state, and unbundled functions can provide more reusability and extension ability.
The thing is that, immutability will make a lot of the use cases for hiding data obsolete, because you will not implement things by using mutable state.
Take a counter for example, I feel this is a classic case:
class Counter {
private count = 0;
public increment() {
this.count++;
}
public getCount() {
return this.count;
}
}
counter = new Counter();
counter.increment();
println(counter.getCount());
Why did you make count
private?
Now in Clojure:
(defn make-counter
[]
{:count 0})
(defn increment
[counter]
(update counter :count inc))
(def counter (make-counter))
(-> counter
increment
:count
println)
Why is it okay to let users read the count
field directly while in OO a getter was used?
I would say this type of “hiding of information” in OO is the most common one, where data is hidden not because you might one day refactor the variable count
and change its name or type or structure, but because you wanted to protect it from being writable by non-vetted code that isn’t aware of how to correctly modify its value with keeping to the conceptual invariants of the data at hand.
What are other reasons you’d want to hide data from consumers? I can only think of two:
- Because you want in the future to be able to refactor the data without breaking consumers
- Because you don’t want to confuse the consumer with what data is relevant to them, versus a detail used internally
Now for these, I think again we’re talking about rare scenarios, because most of the time you will have functions and namespaces that easily let you hide data for those use case behind the function or the namespace.
So it would be in the case where you need an instance-like piece of data, and for which you want the luxury to be able to change this data in the future knowing no one was depending on its details, or you want to be clear as to which fields is relevant to the consumer and which isn’t.
Honestly I’m failing to think of an example for this even. So I’m going to use a made up thing, but this shows you how rare this scenario is.
Alright, so basically for this in Clojure people won’t totally prevent the consumer from ever using the internal data, but instead will rely on convention to let the consumer know some of the data is implementation details and if they were to depend on it it’s at their risk of future breakage. This is often true in OO as well.
One way is with simply being clear in the key name:
(defn make-foo
[]
"Makes a foo, keys under `impl` namespace are internal details, depend on them at the risk of future breakage."
{:impl/detail 0
:impl/other-detail 0
:relevant 0
:also-relevant 0})
You can pick another name if you prefer like private
or do-not-use
, etc.
If you want to make it less polluting of the relevant keys you can nest them all:
(defn make-foo
[]
{:impl/details {:some-detail 0, :some-other-detail 10}
:relevant 0
:also-relevant "Foo"})
Another common way for this is to use Clojure’s metadata:
(defn make-foo
[]
^{:foo/detail 0
:foo/other-detail 10}
{:relevant 0
:also-relevant 10})
I would say metadata is probably the facility that Rich Hickey thought people would use for this, but in practice people have found just sneaking impl details keys on the map to be just as good and less trouble, so I feel a lot of people just use a convention like I showed in the first approach.
If it wasn’t obvious, you can always have local variables inside a function, that hides the details. And in a namespace you can declare private vars as well:
(ns foo)
(def ^:private bar 10)
Generally that covers most use cases for #1 and #2. So using key convention or metadata like I said is only when you need implementation details data in an per-instance way, which is something needed.muxh less often, but as you see there are still straightforward ways to cover this need.
Now there’s a way to absolutely prevent all use of internal details by using function closures, but nobody ever uses that, and I’d advise against it, there’s no real reason to treat your fellow programmers like babies who can’t choose their own risk. Also, it becomes a lot more ackward to use. But for your interest look here to see how: Poor man's objects?
And finally, sorry this is already pretty long, but there is a third reason for information hiding and even encapsulation, that’s:
- Because you do need mutable state and now have all the problems it creates so you need to protect it again from external mutation
For this, Clojure has deftype
, and that’s what you’d use if you were to implement a data-structure in Clojure or other constructs like that which have inherent mutable state and should only be manipulated with invariant protecting functions.