In OO languages like Java, people tend to create new classes more than those that reuse existing ones. In Clojure and other FP languages, we tend more on the reuse side. How do we develop this habit?
Eric Normand: How do you create a habit of reusing instead of doing new things? Hi, my name is Eric Normand and these are my thoughts on functional programming.
In functional programming, at least in the way I'm used to seeing it, we tend to reuse pieces much more than in typical object-oriented programming. Because? I do not think it has anything to do with the paradigm or language. I think it has a lot more to do with the habits, the habits that the programmers have in those various communities.
I will give an example. Actually, Rich Hickey is really talking about how, in the Java world, there is something called servlet. The servlet defines how to process an HTTP request. There's a class called HttpRequest, something like that. It defines all the things you would expect from an HTTP request.
Put the slide on top and the slide has all the methods of the class, just their signatures, and it's pretty clear what they do. They are well named, but he says, what are the hashmaps? Where are all hashmaps? It's a kind of strange question. You're not used to asking, but sometimes things are going pretty well.
There was a method called getHeader. He took a string and returned a string. It's like stepping out of a hash map. It has a key and a value. Then there was also getProperty, and we passed a string and got a string. If I could get getPropertyNames, it would give you a list of all the property names that had been defined on that request.
You could see, yes, these are hashmaps. Why did they define their own small custom protocol for this? Then he says, "Look, there are actually deeper hashmaps you do not think about." Because all these other things that are just methods defined on the class are just getter, so it's just things like getPort, and getHost and getIP.
Why not just create those keys? Those are just the keys, the IP, the host, the port. The whole thing should be a hash map and these are the keys. What he was getting was that in Clojure we have this thing called Ring that defines the hashmap format to represent an HTTP request.
It's really interesting because it means you do not need a new type. You only need this new specification for what goes into the hash map. Everyone already knows how to use a hashmap.
I mean, if you're a Clojure programmer who uses hashmaps, and there's already a lot of features to handle them and they can be serialized. Basically, we are getting all this reuse from hashmaps. When I'm in Java, they have hashmaps. They could have done it.
Instead, they chose to define a new class that required more documentation, requiring re-implement many of these features. Maybe he even uses the hash map internally to represent those headers, they could, could. The point was that in Clojure, this is what we do. We reuse. It looks like a hashmap, just use a hashmap. Why should you create a new type just for that?
In Java, they do not. For me, it's just a habit. Now, how do you develop this habit? This was the question I started with. The habit is really about understanding the two parts. You must understand, number one, what you already have. What are the things you have that you could reuse? You must understand your data types.
You must know that you have vectors, you have hashmaps, you have sets, you have your sequence abstractions. You have all kinds of things you need to learn and get indexed. You must have these types of data indexed by their access templates.
If you look at something and say it's a getProperty, and take a string and return a string, now you have to think, "Well, it's kind of key value, it's probably a hash map." You have to have it automatically.
The second thing you need to do, you will constantly access that index. Instead of thinking, "What new thing should I do?" You should think, "What can I reuse?" So start looking at it in terms of these access models.
Access models will depend on the language. Clojure defines a number. They are standard interfaces that come with the language. Include things like sequence abstraction, which is how you access items one at a time, regardless of whether you're remembering duplicates. This is an access model.
There is access to a value, given the key. He's adding things to a collection. Where are you going to add it? You can add to the front, on the back, if you keep the order. These are all kinds of things you need to think about your data structures.
You can not think of … Unfortunately, I was taught this way. As a list: it's just an ordered collection, but it's not enough. Thinking about it this way is missing a little bit of important information. I was taught Java and you have a list interface and it has these methods on it.
One of the methods is like get and you give it an index, an integer, and it will give you the object in that index, which sounds reasonable. The problem is that when you implement that interface, the result could change the algorithmic complexity.
If you have a list of arrays, which is another type in Java, it can quickly provide the value based on the index because it is implemented with a large array. Arrays can make that random access in the array.
If you use the linked list, which is another type in Java, another class, you can not do it. You actually have to scroll through the list, counting how many you've seen and then returning the last one to that index, so it's linear as the list gets bigger.
To say that both are implementing the same interface, it's kind of a lie because your algorithm could go from a constant to a linear time, or worse, it could go from linear to quadratic. It's accidentally quadratic just because you do not realize it would be possible to access it.
What Clojure has done is to a large extent – it's not perfect in this because it's still possible to get an item in an index – it simply uses a different function to get. If you are on a linked list you can do the nth, but the umpteenth is known to be potentially linear, while joining a list does not work, but getting a vector works because it is a constant time.
These operations, part of the contract is that they maintain the algorithmic complexity. When implementing such interfaces, part of the contract should not be implemented if it can not be constant.
As I said, to a large extent, it's like 90 percent, this is true. There are some exceptions. They are unlucky but, to a large extent, I think Clojure does it the right way. These are the things you have to think about.
If you're a programmer, you're doing anything on any reasonable scale, you have to start thinking about algorithmic complexity, and it should be part of the interface. Those are the things that must be the first choice you make. How do I access this stuff? So obviously, this leads to this type of reuse.
Why re-implement the hash map or even wrap it if I had to define all these new methods? Why not use only what is already there?
My name is Eric Normand. You can reach me on Twitter. I am @ericnormand with a D. You can also email me at [email protected] I hope to hear from you because I like to enter into discussion with people. Awesome. Rock on.