3

The Clojure official spec doc states:

Most systems for specifying structures conflate the specification of the key set (e.g. of keys in a map, fields in an object) with the specification of the values designated by those keys. I.e. in such approaches the schema for a map might say :a-key’s type is x-type and :b-key’s type is y-type. This is a major source of rigidity and redundancy.

And in this SO question: clojure.spec human readable shape?

The following example is given:

(s/def ::car (s/keys :req [::tires ::chassis]))

(s/def ::tires (s/coll-of ::tire :count 4))

(s/def ::tire (s/or :goodyear ::goodyear}
                    :michelin ::michelin))

My question is: how is this not ridig and not redundant? By opposition to that, what would be and example (in Java?) of something rigid and redundant?

As I see it you still cannot define a car that'd be, say, a dragster with 6 wheels because ::tires must have 4 elements. You cannot either define a floating car whose rear wheels would be propellers.

How's the above example different from, say, static typing from the standpoint of rigidity and redundancy? How's that different from a Java car class that'd be constructed using a tires instance itself containing four tire instance?

Basically I think what I don't get is that you're spec'ing the map by telling which keys are required. So far so good. But then the keys are themselved spec'ed, so the values designated by those keys are specified too no!? How are things "not conflated" here?

Cedric Martin
  • 5,945
  • 4
  • 34
  • 66

1 Answers1

2

Just for a moment, zoom back out and think about software development as a practice. We'll get to spec at the bottom.

As engineers, one of our most important skills is the ability to define and think in abstractions.

Think about how function composition simplifies the construction of complex software. It simplifies the definition of a complex function by allowing us to think more broadly about what is happening. A nice benefit of this is that it also allows for the smaller functions which compose the larger one to be reused when writing similar, but slightly different, complex functions.

Of course, you don't need functions at all. You can write an entire project in one function. However, this is a bad idea primarily because it conflates the intent of a function with the specification of the function.

For example, a function make-car calls build-engine, build-drivetrain, install-interior, and a lot more. Sure, you can take the code from each of those and paste them inside make-car. But the result is that you have lost abstraction. make-car cannot be improved or changed except in the make-car code itself. The code for how to build an engine cannot be improved or reused to make any other car. Why? Because the knowledge of how to build that engine for that particular specification of car is embedded in the make-car function.

So, make-car should not define how to build the engine (or any other components of the car); it simply specifies what components make up the car, and how they work together. The specifics of those components do not belong to the working knowledge embedded in make-car.

The comparison to spec should now be clear:

In a similar fashion, spec allows you to define entities as abstractions. Could you embed the knowledge of an entity within a specification/schema? Certainly. Can you directly substitute the specification of the individual components into the entity definition itself? Yes. But in doing so, you are conflating the entity with the definition of its components. The loss is the same as with the above: you lose the abstraction of what the entity is, as now you must change the definition of the entity in order to change details about the entity that are really details of its components; and, you have lost the ability to reuse definitions for similar, but distinct, entities.

Community
  • 1
  • 1
Josh
  • 4,726
  • 2
  • 20
  • 32
  • 1
    thanks for the detailed answer but I really don't get it. The example is not mine: this *car* example comes from an answer from another SO question. Is that *car* example conflating the definition of the *car* entity with the definition of its *tires* component or not? And how's that different from a Java *Car* class whose constructor would take a *Tires* instance? – Cedric Martin Jun 28 '17 at 23:49
  • 1
    @CedricMartin (1/2) Yes, the car example was mine, and I will admit that it could be better. But, think about it this way: if we want to change ANYthing about the details of the *tires* component, does the definition of the *car* entity change? No. So, the two are totally decoupled, so much so that the spec for the components do not even need to exist in order to spec the entity. – Josh Jun 29 '17 at 05:31
  • 1
    @CedricMartin (2/2) It's not *that* different from your Java example, and is very much in line with my explanation of abstraction as a general idea in software, which Java accomplishes via encapsulation, class hierarchies, and interfaces. The main differences is that in `spec` there is no *type* tied to the component, whereas in Java the `Car` constructor must take an object of type *Tire*. – Josh Jun 29 '17 at 05:32
  • oooh gotcha. Thanks a lot for the answer and I didn't realize the *car* example I quoted was yours : ) – Cedric Martin Jul 01 '17 at 00:06