The Author Online Book Forums are Moving

The Author Online Book Forums will soon redirect to Manning's liveBook and liveVideo. All book forum content will migrate to liveBook's discussion forum and all video forum content will migrate to liveVideo. Log in to liveBook or liveVideo with your Manning credentials to join the discussion!

Thank you for your engagement in the AoF over the years! We look forward to offering you a more enhanced forum experience.

Somoza (7) [Avatar] Offline
#1
On page 210, the text reads “You don’t want to pass a symbol as the parent class but rather the var named by that symbol.” I don’t think that line is inaccurate, but it seems debatable.

The corresponding line of code is the last line in Listing 8.1, as follows:
`(def ~class-name (new-class '~class-name #'~parent-class ~fns))))

But if you take away the #' preceding ~parent-class, then the following (from page 211) still works perfectly:
(def donna (Woman :new))
(donna :greet "Shelly")
(donna :age)
(donna :about 3)

In fact, an argument can be made that ~parent-class is better than #'~parent-class in Listing 8.1. Later on, if we redefine the Person class and exclude all methods, as follows:
(defclass Person)

Then following will not work:
(donna :about 3)

But if we use ~parent-class instead of #’~parent-class in Listing 8.1, then (donna :about 3) will still work because the functions wouldn’t point to the same var.

So is the idea of using #’~ instead of ~ to intentionally have shared mutable functions, so that anything that derives from Person/Woman behaves consistently, even if we change the class definition later?

Any thoughts or opinions would be appreciated. Thank you!
Francis Avila (16) [Avatar] Offline
#2
There are intertwined issues here: identity vs value, mutability vs immutability (which you point out), and static vs late binding. But first some OO history.

There are two schools of thought about OO. The first favors static (early) binding and thinking of methods as virtual function invocations with an implicit first argument (which is the struct of its properties). In this model, classes are purely a way to organize functions with the structs (properties) they operate on. In this model, the class often "disappears" at runtime: classes are only known by the compiler. C++ is firmly in this camp. Java and C# use this conceptual model and favor programming in this style, but their classes do exist at runtime and you can do reflection trickery to create and modify classes dynamically--but notice this is considered an advanced technique and a bit of black magic, but in the second school of OO thought creating and modifying classes dynamically is common and easy!

The second school of OO thought favors late binding and thinking of "methods" as "messages the object responds to". This model comes from SmallTalk and is found (to greater or lesser degree) in Objective C, Python, Javascript, Ruby, etc. This is also the model that this chapter is following. You notice that the new-class function returns a function which responds to "commands" (the case statement and first argument) rather than being a virtual function lookup table. Notice also that there are no public "properties" in the Java sense: to change an object's state, you issue a command that changes state rather than edit its state directly.

So in chapter 8, we are creating an OO system that resembles this second school of OO thought. The decision to use a var follows that design.

In general, OO programming favors identity semantics rather than value semantics: one class is distinguished from another by its name not its structure, and instances are distinguished by their address in memory not their state/property/class/etc. In Clojure, identity is modeled with a reference type (atom, agent, etc). Vars are most appropriate for named, mostly-unchaging state, so they are a good choice for modeling a OO class, which is named and mostly-unchanging.

However, as you point out, you could also dereference the var directly when you create a class. But doing this makes classes "immutable"--every instance is locked in to the implementation of the parent class as defined at the moment the child class is declared. This follows the C++ model of OO, except such languages make it impossible to redeclare/modify classes. You could implement this yourself by using defonce instead of def in the defclass macro. If you do this you should also change the find-method function to check for equality with the OBJECT value directly instead of the OBJECT var.

Using defonce makes experimenting with classes at development time (at the repl) more of a headache. You are essentially back to the "recompile the program on every change" model of languages like Java unless you add more magic support functions like Clojure does for multimethods. You basically give up a lot of power and flexibility without any benefit.


The reason to like the C++ OO model is that such programs are much easier to optimize ahead-of-time and can have more predictable runtime characteristics. You can't get any of those benefits by building your object system in Clojure as Clojure (i.e., coexisting with the Clojure runtime as normal Clojure code) the way we do in chapter 8 because Clojure is already a dynamic, late-bound, mutable-var environment. However, you could implement a compiler for your hypothetical C++-like OO language in Clojure. How this would work is that your class-related functions and macros (defclass, new-object, etc) would not implement the object system itself but instead return *descriptions* of the classes/objects you create as a kind of intermediate language or AST. You would then write a compiler in Clojure which consumed these descriptions and produced optimized code. In fact, this is how Clojurescript is implemented!