When to use map vs defrecord?


#1

I ran across this advice from clojure applied on the first chapter modeling entries:

One specific case for which you should strongly consider maps is in public-facing APIs, whether they’re expected to be consumed by Java or by Clojure. In an API, it’s important to minimize the constraints on our callers. Requiring them to create instances of our record classes causes some of the details of those classes to be effectively public as well. In this case, maps with a well-known set of keys commit to less and are a simpler and better choice.

I’m choosing to ignore the next line:

Don’t fret about making the wrong choice between maps and records!

And want to fret a little. Namely, i’m not sure what details of the classes are reviled by a record being sent to a caller that wouldn’t be if it were a map. At this state in my learning a record seems like a set-key map factory. It gives the reader some idea that this data belongs to group of similar instances and is expected to conform to this set of fields. I dont see why the caller wouldn’t benfiet from this knowledge as well.


#2

Hi @Drew_Verlee,

Thanks for the great question!

The short answer is, yes, you can use Records if you are modeling very common domain entities with known keys. Records mostly act like maps. They even respond true to the map? predicate.

The reason people are a little hesitant to use Records, as the paragraph you quoted mentions, is that they require a certain type/class to be used. This increases the surface area of the API. You now have to think about what to do as the API changes over time. How will the Record change? If you make a required attribute optional, no change is needed in the clients. However, the Record will still have that key until you change it. How can you change it without disrupting existing clients? If you remove the attribute from the defrecord, the constructor will change. Or you could store nil in the now optional attribute when it wasn’t set, but you’re creating a special case.

Long story short: even if you make a Record for clients to construct, don’t force the clients to use them. Let them use maps as well. That will make sure it stays generic.

The question goes deep into the heart of the philosophical differences with Clojure. The philosophical choice of Clojure is to use generic data types because the operations can be made generic. The only time I really use Records is to make a new class to hook protocols to.

If you’re interested in helping clients figure out what data they can pass, the new clojure.spec library is the best bet. Otherwise, you can also create your own factory functions to help clients build the maps they can use in your API.

Eric


#3

Thanks for your reply Eric! I think understanding the answer to this question will lead to some major mental breakthroughs for me, so i appreciate your time on the subject!

I’m not sure I see how requirement changes if we were using a map.

The difference i see is that a record implies a protocol and if the client doesn’t have access to that protocal then the record is basically a map. In which case its better to use maps as they keep the system (client and server) more generic. Which is what i believe your saying here:

The philosophical choice of Clojure is to use generic data types because the operations can be made generic.

As in, sense your clients wont have access to the protocals, better to not design your data around it, as this leads to coupling between code and data?

Like in the examples here (using deftype) http://blog.klipse.tech/clojurescript/2016/04/09/clojurescript-protocols-secret.html

We now have two animals: Pluto and Tweety can both speak. But if i sent those records to the client, without the protocal, then the client wouldnt have any of the logic around the animals talking. It would be better to build a map that contained the string they returned when they spoke. E.g

{:Animal Dog :says "bark bark"}

Here I Keep all the domain knowledge as data so it can be passed around between systems.

Which leaves me wondering when records and protocals are useful outside building the core language up. You seem to imply you use Records inside your appliactions,

The only time I really use Records is to make a new class to hook protocols to.

but i suppose i dont know when this would be appropriate.


#4

Hi @Drew_Verlee,

I should have qualified my statement: I almost never use Records. The only time I do are if they are required by a library to use a protocol, such as with the popular Component library. Protocols are mostly useful for unifying access to different types. The JVM ecosystem has a huge sent of existing types, so this is often useful. Proliferating types is not something I like doing in Clojure, if I can help it. I like structured data and functions. Protocols are also useful for open polymorphism. That is, polymorphism where you expect new types to participate later.

Your example of animals talking is good. It all comes down to your requirements. Clojure provides a bunch of tools, each good for a particular task. If you need to stream it over the network or save it to a database, pure data is preferred. If you expect that you want an open system where new classes participate in a “Talking” protocol, then a protocol might be what you want. It really depends on what you need.

Now, about Records and external APIs. Records define their fields as part of the record, and each field has to have a value (they can be nil). Let’s say you make a Record called Animal that had a species (:dog, :cat, etc) and a speech (:bark, :meow, etc). People start using it, and everyone is happy. And they’re asking for new features. So you want to capture the animal’s color. How do you do that? The Record type is fixed. It has two fields. If you add a third, you’re going to break backwards compatibility. So you could make an Animal2 which is like Animal but also has a color field. And your functions now accept either. But there were some clients who always expected Animals to be returned from some functions. They defined their own protocols on Animal. But now you’re returning an Animal2!!! Their code is not working.

If you had just used a map, this wouldn’t have happened. The limitations of maps are what make this possible. It’s always the same Associative interface, so you can’t really branch on it. You just have to dig out the data you need. And digging out data is exactly what maps are good at.

I hope that helps. It’s awesome that you’re thinking about this stuff, because it goes to the heart of the Clojure philosophy.

Rock on!
Eric


#5

Hi @Drew_Verlee ,

what @ericnormand said is absolutely true. I also rarely use records, but when I do it’s for Java interop.

I want to add that “outside” of your system might also be seemingly close things like your UI, Database or file persistence. So “the caller” might be you in a year from now trying to read data from an old edn-file and failing because the record has changed in the meantime.
When you want to communicate to your callers what the shape of your response is, use maps and clojure.spec. The caller can always read a map and does not need the spec to do that (which is one of the reasons specs are so great).

About records in general: they are faster and require less memory.
This is due to the fact that records are not maps. They are objects that simulate being maps. When accessing “:a” in record the value of the instance’s field “a” will be retrieved. That means as an object the record requires a lot less memory, because it saves its values in fields and does not require any mapping and corresponding keys to do so. Access is faster because it requires no hash lookup, only access to the instance’s fields. Long story short: records are closer to the metal than maps. When you’re handling thousands of objects, this might make a difference.

hth

Azel