8

My background is Ruby, C#, JavaScript and Java. And now I'm learning Clojure. What makes me feel uncomfortable about the later is that idiomatic Clojure seems to neglect the Uniform access principle (wiki, c2) and thus to a certain degree encapsulation as well by suggesting to use maps instead of some sort of "structures" or "classes". It feels like step back. So a couple of questions, if anyone informed:

  • Which other design decisions/concerns it conflicted with and why it was considered less important?
  • Did you have the same concern as well and how it end up when you switched from a language supporting UAP by default (Ruby, Eiffel, Python, C#) to Clojure?
7
  • please add uniform-access-principle tag who has enough reputation.
    – Alexey
    Commented Oct 15, 2013 at 0:35
  • 10
    Because clojure isn't object oriented. UAP is principly meant to distinguish between methods and fields. Clojure has no objects, everything is either data or a function. So access is uniform. Commented Oct 15, 2013 at 2:44
  • @jozefg, you just replace methods with functions and fields with hash keys and you have the same case for Clojure. And (:f x) and (f x) are different and access is not uniform. So your statement is not an argument.
    – Alexey
    Commented Oct 15, 2013 at 12:33
  • 6
    youre missing the point, functional programming languages dont have "smart data". Abstraction comes through functions. Define a function some-key and change it however you want if you want to access it differently. I'd encourage you to try it. Ive been writing lisp for 4 years and missed this idea exactly 0 times Commented Oct 15, 2013 at 13:58
  • @Alexey It isn't the same issue because they aren't the same thing. (:f x) and (f x) are doing the same thing calling a function on an argument. UAP at its most basic level. If you want to hide which method you are calling then that is a different matter.
    – stonemetal
    Commented Oct 15, 2013 at 16:58

3 Answers 3

15

First, a disclaimer: I'm not deeply familiar with the uniform access principle so you may want to take this with a grain of salt. That said, I would argue that Clojure does observe a uniform access principle: function calls.

The key quote on Wikipedia seems to be that "all services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation", and that's exactly the case with function calls in Clojure. In fact, everything in Clojure (and other Lisps) is a function call except for special forms and macros - and macros actually are functions, with the distinction that they operate on source code. You can even specify function call behavior for your own types by implementing clojure.lang.IFn:

(deftype Invokable []
  clojure.lang.IFn
  (invoke [this]
    :was-invoked))

(def invokable (Invokable.))
(invokable)                  ;=> :was-invoked

You may have in mind the use of keywords as functions, which are most specifically associated with hash-maps, but you can actually implement that behavior for different types as well. Here's a silly example where keyword access reads a file (with the same name as the keyword) from your home directory, or returns a default value if such a file doesn't exist:

(ns weird.example
  (:require [clojure.java.io :as io]))

(deftype WeirdKlass []
  clojure.lang.ILookup
  (valAt [this k not-found]
    (let [home (System/getProperty "user.home")
          file (io/file home (name k))]
     (if (.isFile file)
       (slurp file)
       not-found)))
  (valAt [this k]
    (.valAt this k nil)))

(def klass (WeirdKlass.))

;; assuming you have a file at $HOME/testfile
(:testfile klass)        ;=> "test content!"
(:blah klass)            ;=> nil
(:blah klass :not-found) ;=> :not-found

Update based on Alexey's comment

I think I have a better idea what you mean now but in practice I haven't found it to be an inconvenience. When you want or need a level of indirection/abstraction between your code's functionality and its underlying data representation the way to achieve it is: functions!

The simplest way would be to define full-name as a function which happens to use keyword lookup but could later be changed to instead compute the name (without clients knowing or caring):

;; For now
(defn full-name [person]
  (:full-name person))

;; Maybe later
(defn full-name [person]
  (str (:first-name person)
       " "
       (:last-name person)))

Of course, you could also use records and protocols:

(defprotocol IPerson
  (full-name [p])
  (first-name [p])
  (last-name [p]))

(defrecord Person1 [name]
  IPerson
  (full-name [_]
    name)
  (first-name [_]
    (first (.split name " ")))
  (last-name [_]
    (last (.split name " "))))

(def person1 (->Person1 "Joe Schmoe"))
(full-name person1)  ;=> "Joe Schmoe"
(first-name person1) ;=> "Joe"
(last-name person1)  ;=> "Schmoe"

(defrecord Person2 [f-name l-name]
  IPerson
  (full-name [_]
    (str f-name " " l-name))
  (first-name [_]
    f-name)
  (last-name [_]
    l-name))

(def person2 (->Person2 "Joe" "Schmoe"))
(full-name person2)  ;=> "Joe Schmoe"
(first-name person2) ;=> "Joe"
(last-name person2)  ;=> "Schmoe"

The big (very big, in my view) upside of using "plain" data structures when possible is that it means you and your users have Clojure's full range of map and sequence operations at your disposal. Alan Perlis expressed this idea well when he said that "it is better to have 100 functions operate on one data structure than 10 functions on 10 data structures."

2
  • To clarify what I meant saying UAP: assume in Ruby in class User I have an attribute name. So I access it like user_instance.name. But then I can replace the name attribute with a method that computes it using first_name and last_name. And I don't need to update the client code. In case of Clojure I would have to replace in client code (:name ...) with (user/name ...) e.g. to have the same effect.
    – Alexey
    Commented Oct 15, 2013 at 12:46
  • @Alexey, thanks for the clarification - I updated my answer.
    – jbm
    Commented Oct 15, 2013 at 16:36
8

In short: it's considered not valuable enough to bother introducing. Replace a representation and re-factor/modify dependent code when you need it.

From an interview:

Fogus: Following that idea—some people are surprised by the fact that Clojure does not engage in data-hiding encapsulation on its types. Why did you decide to forgo data-hiding?

Hickey [creator of Clojure]: Let’s be clear that Clojure strongly emphasizes programming to abstractions. At some point though, someone is going to need to have access to the data. And if you have a notion of “private”, you need corresponding notions of privilege and trust. And that adds a whole ton of complexity and little value, creates rigidity in a system, and often forces things to live in places they shouldn’t. This is in addition to the other losing that occurs when simple information is put into classes. To the extent the data is immutable, there is little harm that can come of providing access, other than that someone could come to depend upon something that might change. Well, okay, people do that all the time in real life, and when things change, they adapt. And if they are rational, they know when they make a decision based upon something that can change that they might in the future need to adapt. So, it’s a risk management decision, one I think programmers should be free to make.

If people don’t have the sensibilities to desire to program to abstractions and to be wary of marrying implementation details, then they are never going to be good programmers.

1

I'll add one more answer related to the comment:

To clarify what I meant saying UAP: assume in Ruby in class User I have an attribute name. So I access it like user_instance.name. But then I can replace the name attribute with a method that computes it using first_name and last_name. And I don't need to update the client code. In case of Clojure I would have to replace in client code (:name ...) with (user/name ...) e.g. to have the same effect

First off, to be clear, Clojure does believe in encapsulation for hiding implementation details, and Clojure offers a lot of ways to encapsulate implementation details when needed, such as private vars or deftypes.

It does not believe in encapsulating information though.

Back to your example now, if you were to encapsulate this information behind a method inside a class you'd have:

class Person
  + getName

So at first our information was a single name. Now we've somehow managed to know the distinction of first and last, so we want to have firstName and lastName, you'll probably do this:

class Person
 + getName
 + getFirstName
 + getLastName

Now look at this a few time and tell me what's the point of these methods over just a map which would have started as:

{:name "string"}

And then becomes:

{:name "..."
 :first-name "..."
 :last-name "..."}

Clojure doesn't see any real benefit in wrapping information, information is passive, it is just data, so better to keep it as data.

Here's a bigger walkthrough. First we model a Person as a simple map with :name key.

(defn make-person
  [name]
  {:name name})

Now someone starts to depend on this information:

(defn say-hello
  [person]
  (println "Hello " (:name person)))

(-> (make-person "Johny Cash")
    (say-hello))

Now we somehow change to collect people's first and last name separately, so we refactor, but don't want to break existing use of our information model:

(defn make-person
  [first-name last-name]
  {:name (str first-name " " last-name)
   :first-name first-name
   :last-name last-name}))

(-> (make-person "Johny" "Cash")
    (say-hello)) ; It still works!

If we had encapsulated the information we'd have done either the same thing:

class Person {
  Person(String firstName, String lastName) {
    name = firstName + " " + lastName;
    this.firstName = firstName;
    this.lastName = lastName;
  }
  String getName() {
    return this.name;
  }
}

sayHello(Person person) {
  System.out.println(person.getName());
}

Or we'd have done this:

class Person {
  Person(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  String getName() {
    return firstName + " " + lastName;
  }
}

sayHello(Person person) {
  System.out.println(person.getName());
}

Only that second refactor is not possible in Clojure, but as we said, information is passive, in that way there's no real benefit to that second way of refactoring over the first.

Clojure basically is saying that UAP provides no real benefit while adding a lot of additional verbosity and being less malleable because if your data is actually a computation or a complex object type with methods, it can no longer be easily serialized, introspected, enumerated, transformed, etc. What if you want to persist it to disk? Or store it in a database?

You also need to keep in mind this whole time that data is immutable in Clojure as well, and the functional style of programming strongly favors purity, so the idea that you can call person.name multiple times in a row and get a different name because it would be dynamically computed or queried from somewhere external that is mutable is a big no no for Clojure as well. And if all your fields are going to be immutable and referentially transparent, what's the point of computing them over and over or querying them over and over? Just gather/compute the data at creation of the structure and you're done, like I did in my example.

Finally, if for some reason you really need a way to do this, it is actually possible to do, there are libraries that provide something generally known as a lazy-map such as https://github.com/Malabarba/lazy-map-clojure or https://github.com/originrose/lazy-map. What these do is they let you define the value of a map as a function which will be realized only the first time the key for it is looked up and this provides a Unified Access as the lazy-map has the same interface as a normal map.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.