21

In this video by Rich Hickey, the Clojure creator, he advises to use map to represent data instead of using a class to represent it, as done in Java. I don't understand how it can be better, since how can the API user know what the input keys are if they are simply represented as maps.

Example:

PersonAPI {
    Person addPerson(Person obj);
    Map<String, Object> addPerson(Map<String, Object> personMap);
}

In the second function, how does the API user know what the needed inputs are for creating a person?

4
  • 3
    related (possibly a duplicate): Using Map in Java (or other strongly-typed languages) to work with unstructured data
    – gnat
    Feb 6, 2015 at 8:33
  • I'd also like to know this and I feel that the example question does't quite answer it.
    – sydan
    Feb 6, 2015 at 9:08
  • I know I've seen this discussion before somewhere on SE. I believe it was in the context of JavaScript, but the arguments were the same. Can't find it though. Feb 6, 2015 at 10:31
  • 2
    Well since Clojure is a Lisp, you should do things appropriate to Lisp. when you use Java, code in... well Java.
    – AK_
    Feb 7, 2015 at 11:07

4 Answers 4

12

Exagg'itive Summary (TM)

You get a few things.

  • Prototypal inheritance and cloning
  • Dynamic addition of new properties
  • Co-existence of objects of different versions (specification levels) of the same class.
    • Objects belonging to the more recent versions (specification levels) will have extra "optional" properties.
  • Introspection of properties, old and new
  • Introspection of validation rules (discussed below)

There's one fatal drawback.

  • Compiler doesn't check for misspelled strings for you.
  • Automatic refactoring tools won't rename property key names for you - unless you pay for the fancy ones.

The thing is, you can get introspection by using, um, introspection. This is what usually happens:

  • Enable reflection.
  • Add a large introspection library into your project.
  • Mark off various object methods and properties with attributes or annotations.
  • Let the introspection library do the magic.

In other words, if you don't ever need to interface with FP, you don't have to take Rich Hickey's advice.

Last, but not the least (nor the prettiest), although using String as property key makes the most straightforward sense, you don't have to use Strings. Many legacy systems, including Android™, uses integer IDs extensively through the entire framework to refer to classes, properties, resources, etc.

Android is a trademark of Google Inc.


You can also make both worlds happy.

For the Java world, implement the getters and setters as usual.

For the FP world, implement the

  • Object getPropertyByName(String name)
  • void setPropertyByName(String name, Object value) throws IllegalPropertyChangeException
  • List<String> getPropertyNames()
  • Class<?> getPropertyValueClass(String name)

Inside these function, yes, ugly code, but there are IDE plugins that will fill that up for you, using... uh, a smart plugin that reads your code.

The Java side of things will be just as performant as usual. They'll never use that ugly part of the code. You might even want to hide it from Javadoc.

The FP side of the world can write whatever "leet" code they want, and they typically don't yell at you about the code being slow.


In general, using a map (property bag) in place of object is commonplace in software development. It is not unique to functional programming or any particular types of languages. It may not be an idiomatic approach for any given language, but there are situations which calls for it.

In particular, serialization/deserialization often requires a similar technique.

Just some general thoughts regarding "map as object".

  1. You still have to provide a function for validating such a "map as object". The difference is that "map as object" allows for more flexible (less restrictive) validation criteria.
  2. You can easily add addition fields to the "map as object".
  3. To provide a specification of the minimum requirement of a valid object, you will need to:
    • List the "minimally required" set of keys expected in the map
    • For each key whose value needs to be validated, provide a value validation function
    • If there are validation rules that need to check multiple key values, provide that as well.
    • What's the benefit? Providing the specification this way is introspective: you can write a program to query the minimally required set of keys, and to obtain the validation function for each key.
    • In OOP, all of these are rolled up into a black box, in the name of "encapsulation". In place of machine-readable validation logic, the caller can only read human-readable "API documentation" (if fortunately it exists).
5
  • commonplace seems a little strong to me. I mean it's used as you describe, but it's also one of those notoriously unweildy/fragile things (like byte arrays or bare pointers) that libraries try their damnedest to hide away.
    – Telastyn
    Feb 6, 2015 at 23:04
  • @Telastyn This "ugly head of a thousand snakes" typically occur on the communication boundary between two systems, where for some reason the communication or inter-process channel do not allow objects to be teleported intact. I guess new techniques such as Protocol Buffers almost eliminated this archaic use case of map as object. There might still be other valid use cases, but I have little knowledge of that.
    – rwong
    Feb 6, 2015 at 23:11
  • 2
    As for fatal drawbacks, agree. But, if the "easy to misspell" and "hard to refactor" property key names are kept, as much as possible, in constants or enums, that issue goes away. Of course, it does limit extensibility some :-(.
    – user949300
    Feb 7, 2015 at 1:18
  • If the "one fatal drawback" really is fatal, why are some people able to use it effectively. Also, classes and static typing are orthogonal -- you can define classes in Clojure, even though it's dynamically typed. Jan 20, 2017 at 2:26
  • @NathanDavis (1) I admit my answer is written from a static typing perspective (C#) and I wrote this answer because I share the same viewpoint of the asker. I admit I lack a FP-centric viewpoint. (2) Welcome to SE.SE, and since you are a respected figure in Clojure, please take time to write in your own answer if the existing ones aren't satisfactory. Downvotes subtract reputations and new answers attracts upvotes which adds up reputations quickly. (3) I can see how "incomplete objects" can be useful - you can query 2 properties for a given object (name, avatar) and leave out the rest.
    – rwong
    Jan 25, 2017 at 8:29
9

That's an excellent talk by someone who really knows what he's talking about. I recommend readers watch the entire thing. It's only 36 minutes long.

One of his main points is that simplicity opens opportunities for change later on. Choosing a class to represent a Person provides the immediate benefit of creating a statically-verifiable API, as you pointed out, but that comes with the cost of limiting opportunities or increasing costs for change and reuse later on.

His point is that using the class might be a reasonable choice, but it should be a conscious choice that comes with full awareness of its cost, and programmers traditionally do a very poor job of noticing those costs, let alone taking them into consideration. That choice should be reevaluated as your requirements grow.

The following are some code changes (one or two of which were mentioned in the talk) that are potentially simpler using a list of maps compared to using a list of Person objects:

  • Sending a person to a REST server. (A function created to put a Map of primitives into a transmittable format is highly reusable and might even be provided in a library. A Person object is likely to need custom code to accomplish the same job).
  • Automatically construct a list of people from a relational database query. (Again, one generic and highly-reusable function).
  • Automatically generate a form to display and edit a person.
  • Use common functions to work with person data that is highly non-homogeneous, like a student versus an employee.
  • Get a list of all persons who reside in a certain zip code.
  • Reuse that code to get a list of all businesses in a certain zip code.
  • Add a customer-specific field to a person without affecting other clients.

We solve these kinds of problems all the time, and have patterns and tools for them, but rarely stop to think about if choosing a simpler, more flexible data representation in the beginning would have made our job easier.

8
  • Is there a name for this? Say, Object-Property Mapping or Object-Attribute Mapping (along the same line as ORM) ?
    – rwong
    Feb 6, 2015 at 23:14
  • 4
    Choosing a class to represent a Person provides the immediate benefit of creating a statically-verifiable API... but that comes with the cost of limiting opportunities or increasing costs for change and reuse later on. Wrong, and incredibly disingenuous. It improves your opportunity for changing later on, because when you make a breaking change, the compiler will automatically find and point out for you every place that needs to be updated to bring your entire codebase up to speed. It's in dynamic code, where you can't do that, that you really get coupled to previous choices! Feb 7, 2015 at 0:58
  • 4
    @MasonWheeler: What you're really saying is that you value compile-time type safety over the more dynamic (and more loosely-typed) data structures. Feb 7, 2015 at 1:05
  • 1
    Polymorphism is not a concept restricted to OOP. In case of maps you may have inclusive polymorphism (if the elements are subtypes of some type that map can handle) or ad-hoc polymorphism (if the elements are tagged unions). This are internals. The operations that can be performed on a map can also be polymorphic. Parametric polymorphism when we use higher-order function on elements or ad-hoc when dispatching. Encapsulation can be achieved with namespaces or other forms of visibility management. Fundamentally the isolation of objects doesn't equals assigning operations to data types.
    – siefca
    Nov 11, 2015 at 20:04
  • 1
    @GillBates why do you say that? You just lose the opportunity to put those virtual methods "inside the Map" - but that's exactly what Rich Hickey talks about, "ActiveObjects" are really an anti-pattern. You should treat data as what it is (data), and not intertwine it with behaviour. There are huge simplicity benefits to be achieved by separating concerns.
    – Virgil
    Nov 11, 2016 at 8:30
4
  • If the data has little or no behavior, with flexible content that is likely to change, use a Map. IMO, a typical "javabean" or "Data Object" that consists of an Anemic Domain Model with N fields, N setters and N getters, is a waste of time. Don't try to impress others with your glorified struct by wrapping it in a fancy smancy class. Be honest, make your intentions clear, and use a Map. (Or, if it makes any sense to your domain, a JSON or XML object)

  • If the data has significant actual behavior, a.k.a methods (Tell, Don't Ask), then use a class. And pat yourself on the back for using real Object Oriented programming :-).

  • If the data has a lot of essential validation behavior and required fields, use a class.

  • If the data has a moderate amount of validation behavior, that's borderline.

  • If the data fires property change events, that is actually easier and far less tedious with a Map. Just write a little subclass.

  • One main drawback of using a Map is that the user has to cast the values to Strings, ints, Foos, etc. If this is highly annoying and error-prone, consider a class. Or consider a helper class that wraps the Map with the relevant getters.

1
  • 1
    Actually, what Rich Hickey argues is that If the data has significant actual behaviour ... you're probably doing the whole "design" thing wrong. Data is "information". Information, in the real world is NOT "a place where data is stored". Information does not have "operations that control how information changes". We do not convey information by telling people where it's stored. The object-oriented metaphors are SOMETIMES an appropriate model of the world... but more often than not, they aren't. That's what he says - "think about ypur problem". Not everything is an object - few things are.
    – Virgil
    Nov 11, 2016 at 8:37
0

The API for a map has two levels.

  1. The API for maps.
  2. The conventions of the application.

The API can be described in the map by convention. For example the pair :api api-validate can be placed in the map or :api-foo validate-foo could be the convention. The map can even store api api-documentation-link.

Using conventions lets the programmer create a domain specific language that standardizes access across "types" implemented as maps. Using (keys map) allows for determining properties at runtime.

There's nothing magical about maps and there's nothing magical about objects. It's all dispatch.

Your Answer

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

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