"You gain power and lose nothing" (question about quote from Clojure for the Brace and True)

This comparison also starts to reveal some limitations of object-oriented programming (OOP). In OOP, one of the main purposes of classes is to protect against unwanted modification of private data—something that isn’t necessary with immutable data structures. You also have to tightly couple methods with classes, thus limiting the reusability of the methods. In the Ruby example, you have to do extra work to reuse the clean! method. In Clojure, clean will work on any string at all. By both a) decoupling functions and data, and b) programming to a small set of abstractions, you end up with more reusable, composable code. You gain power and lose nothing.

(from Functional Programming | Clojure for the Brave and True)

Is this true? Encapsulation is valuable more so than just disallowing unwanted modification. Hiding implementation details is just as important in my opinion, and immutability doesn’t help here.

1 Like

This might be a naive question but why is hiding implementation details important?
How would you hide them if you’re working with data?

1 Like

What other value does it provide?

Yes, I think it is true. There are no invariants to protect with regards to modifying the state of some data when that state is not allowed to be modified in the first place.

When data is shared by more than one thing, if something changes it in a broken way, it will break other users of it as well. When mutation is not involved, each thing using the data gets its own copy, so if one wants to change it in strange ways, others won’t be broken by the change. The data does not need to be encapsulated in order to prevent this, the data being immutable already prevents it.

And by unbundling the methods from the data, the methods (now functions), can easily become more generic to any data which quacks to the correct shape the function expects, or made polymorphic on any property of the data passed into the function. That does help make things more reusable as well as allow external extension of what can be done to some data.

The quote from Brave and True doesn’t talk about hiding implementation details, it says that one use of classes is to protect from unwanted modification of some shared data, which immutability renders a non issue, as shared data is not allowed to be modified without copying. And it talks about bundling of data and operations on this data together (fields + methods), which does couple the methods to the specific data to some extent and can hurt reuse, though it has some benefits like making it easier to find all the implemented operations on some data in some given code base.

If you want to hide implementation details, functions do just that. For example:

(defn make-glamour-shot-caption
  [caption]
  {:caption caption})

(defn clean
  [glamour-shot-caption]
  (update glamour-shot-caption :caption #(s/replace (s/trim %) #"lol" "LOL")))

(def best (make-glamour-shot-caption "My boa constrictor is so sassy lol!  "))

(:caption (clean best))
;;=>  "My boa constrictor is so sassy LOL!"

All implementation details are hidden behind functions, you don’t need classes and objects for this.

2 Likes

It is important, because you might not want your API clients to rely on implementation details. If implementation details are hidden, you can change them freely.

How would you hide them if you’re working with data?

I don’t know how to go about this in Clojure, but in Go/Ruby/Java/C++ I would make the struct/class fields unexported/private.

A convention I sometimes use is to put “impl” in the key, if that data is considered an implementation detail and I’m not ready to support this as a public API. E.g. {:sci.impl/stacktrace ...}.

3 Likes

The quote from Brave and True doesn’t talk about hiding implementation details

I think it does:

one of the main purposes of classes is to protect against unwanted modification of private data

Private data are implementation details.

You gain power and lose nothing

If I lose nothing, I do not lose the ability to hide implementation details.

If you want to hide implementation details, functions do just that.

Functions hide implementation details of operations, but not of data. In OOP you can hide implementation details of data by making fields unexported/private.

The way we usually go about it in Clojure is by putting things in impl namespaces or private functions which can’t be required. That’s a convention which tells the user “don’t rely on this, not a public API”.
Then you’d have a bunch of namespace like my.app.impl.{foo,bar,bazz}.
Data can also be namespaced like in Borkdude’s example

1 Like

Check out this data I hid…

(defn- adder [secret-number]
   (fn [x] (+ x secret-number)))

(def public-adder (adder 2)) ;;nobody knows it's 2 at runtime unless they read the source (still possible) 

similarly

(let [secret-number 2]
  (defn public-adder [x] (+ x secret-number)))

mutable fields are private; if we never mutate them, then they are private immutable fields:

(deftype blah [name ^:unsynchronized-mutable id]
 clojure.lang.IFn
  (invoke [this] {:name name :id id})
 clojure.lang.Associative
  (assoc [this k v] (if (= k :name) (blah. v id) this))
  (valAt [this k] (when (= k :name) name))
  (valAt [this k not-found] (or (.valAt this k) not-found)))

(def ids (atom 0))
(defn ->blah [name]
  (blah. name (swap! ids inc)))

I still almost never use this style in practice. Closures are mostly useful for closing over state for convenience and expressiveness, not hiding it (but they can hide just the same). Encapsulation for hiding implementation details…particularly when a consenting adult might need to know those details in order to either fix or extend the implementation, is really limiting and cumbersome. I think it’s overrated at best. These problems can be better solved with organizing and naming, IMO, without constricting consumers.

2 Likes

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:

  1. Because you want in the future to be able to refactor the data without breaking consumers
  2. 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:

  1. 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.

4 Likes

This article of mine Holy on Dev: Clojure vs Java: The benefit of Few Data Structures, Many Functions over Many Unique Classes shows an example of when Java’s information hiding is pain in the * and how Clojure makes it much better.