Dynamically typed. As a dynamic language Clojure is less capable than many other Lisp implementations/languages. See for example 'late binding'. In Clojure you either need to define a function before its use or need declare it. Not that 'dynamic'. In a typical Lisp dialect the order of definitions makes much less a difference, since functions can be called late-bound. For an interpreter-based implementation, it makes no difference at all.
> It promotes combinations of built-in data structures (lists, maps, vectors) over objects
An aspect I don't like. This makes debugging of larger systems more difficult, since maps are maps. Whereas objects have a type tag built in, have a list of allowed fields, etc. etc. They simply have more more explicit and standardized structure to them.
> Some built-in features like reducers and transducers rely heavily on composing higher order functions for transforming other functions.
Which makes code complex, since transducers are a mildly complex mechanism.
> It encourages REPL-based development
In relatively weak form. Basic features of Lisp need to be added to do simple tasks: like 'instrumenting' code to debug it, since it lacks an interpreter or more capable compiler support. REPLs with actual interactive error handling are not standard. Interactive Lisp compilers like the one from SBCL give much more information and warnings.
Thus the Lisp development features are at best only medium-level.
> Historically, Lisp programmers weren't the biggest proponents of OOP
Funky, people from the Lisp community helped to shape OOP and explored much of it. From guys like Hewitt who developed the actors paradigm in the early 70s, Minsky who developed the frame theory mid 70s and inspired a whole range of OO programming in Lisp, to Howard Cannon who developed Flavors in 1979 with flexible mixins and multiple inheritance used to implement the object-oriented operating system parts of the MIT Lisp Machine, to Lieberman who developed OOP with prototypes mid 80s, to Kiczales who worked a decade on OO in Lisp (LOOPS, CLOS, Meta-object Programming, Object-oriented Protocols, Meta-level architectures, early Aspect oriented programming, ...).
Actually objects in the early Lisp were simply symbols with their property lists. These property lists could hold both normal attributes and also functions.
> An aspect I don't like. This makes debugging of larger systems more difficult, since maps are maps. Whereas objects have a type tag built in, have a list of allowed fields, etc. etc. They simply have more more explicit and standardized structure to them.
You can create explicit and standardised structures for maps in Clojure, or indeed any other data structure:
However, Clojure takes a somewhat divergent philosophy, as it encourages writing schema for individual fields, rather than a schema for a grouping of fields (such as an object).
The namespaces of each keyword are different, but they're grouped in the same map as they happen to refer to the same entity.
So rather than operating on a fixed type like `Student`, a function would request that it requires a data structure that has a person's name and a student ID:
We're still validating (albeit dynamically), but we can be more flexible in what data we ask for. This ties in with Clojure's idea of simplicity; a function shouldn't know about data it doesn't intend to use.
> Then there are defstruct and deftype. So it has maps, struct-maps, records, deftypes, Java classes, ... a whole bunch.
defstruct has been deprecated for years. deftype and Java classes are primarily for JVM interop and language extensions.
Outside of calling Java libraries there are just maps, which you're going to be using 95% of the time, and records, which are maps with efficient polymorphism.
> 'simplicity'?
In Clojure parlance, "simplicity" refers to interconnectedness. So a box of tools is "simple", because they're not connected, whereas a Swiss army knife would be "complex", even if it might be easier for a novice to use.
Having a bunch of similar tools isn't complex, from Clojure's point of view, as long as they're separate. It may make things more difficult to learn, but that's another problem entirely :)
> I would use a class for that, since something like CLOS supports multiple-inheritance and all the necessary mechanisms for mixins.
Doesn't that mean you'd need a separate class for each way you access the data if you wanted to type that? My knowledge of CLOS is, I'm afraid, rudimentary, but can you say to a method: take as as argument an object that has the "id" field from the "Student" class, and the "email" field from the "User" class. Or would you have to explicitly pre-define mixins like HasStudentId and HasUserEmail?
> In Clojure parlance, "simplicity" refers to interconnectedness
Clojure tends to redefine words describing concepts with a new or restricted meaning. Typically a general concept like 'simplicity' would have more than one dimension.
> take as as argument an object that has the "id" field from the "Student" class, and the "email" field from the "User" class. Or would you have to explicitly pre-define mixins like HasStudentId and HasUserEmail?
I would not do this at all. Having slots or not are low-level artefacts and writing methods which are fine-grained to slots of an objects don't expose the domain-level.
I would use methods for domain level classes which have the necessary slots either local or inherited.
> Clojure tends to redefine words describing concepts with a new or restricted meaning. Typically a general concept like 'simplicity' would have more than one dimension.
Sure, but that happens all the time in programming. When we talk about "objects" we don't mean "a material thing that can be seen and touched".
> I would not do this at all. Having slots or not are low-level artefacts and writing methods which are fine-grained to slots of an objects don't expose the domain-level.
I guess because it's not at all idiomatic, even in an object system as rich as CLOS.
But in Clojure it is idiomatic, and to my mind it leads to a more precise definition of what a function wants. We don't say, "this is a method that operates on a Student object"; we say, "this is a function that takes data that includes a student's email and enrolment date".
Again, I hasten to add that my experience with CLOS is virtually non-existent, but compared to the OOP languages I am familiar with, Clojure has a far richer way of describing the structure of data.
I understand that, but as I said I don't like that at all. It binds methods to low-level adhoc features and not to class based domain ontologies. There are other pattern directed invocation systems, which also support that, but I usually prefer the more systematic approach of CLOS. In the tradition of symbolic programming I want to have symbolic classes as my anchors for functionality, not adhoc patterns.
You can still create class-like ontologies with Clojure specs, but usually you don't need to.
Clojure's specs, adhoc or otherwise, certainly might not be your cup of tea. Clojure really pushes the idea of separation and isolation, and in my view you need to buy into that philosophy to be at all comfortable with the language.
Where I disagree with you is your assertion that maps in Clojure are always less structured than objects. I'd claim that (at least when properly spec'ed out) they provide far more structure than objects.
Yes, but as I said, using data structures over objects doesn't mean the data is unstructured. You can use lists, maps and vectors and still have a well defined and validated structure.
"this is a function that takes data that includes a student's email and enrolment date"
You can do this using a row type, e.g. OCaml:
# let foo student =
Printf.printf "Email: %s\nEnrolment: %d" student#email student#enrolment
val foo : < email : string; enrolment : int; .. > -> unit = <fun>
The second line is the output from the OCaml REPL. Note how it automatically infers that the input is an object which includes an email and an enrolment field, with the correct types.
> Historically, Lisp programmers weren't the biggest proponents of OOP
Yes, this is totally misleading... Lisp had OOP before Common Lisp existed, for example the Flavors OOP system through the '70s. This, of course, is before C++ was even a thought in the mind of Bjarne Strostroup.
Also, interesting fact: Common Lisp was the first Object-Oriented language to get an ANSI standard.
> Actually objects in the early Lisp were simply symbols with their property lists.
I was quite surprised to see that this already existed, together with the "property" terminology, in McCarthy's primordial "pre-LISP" list processing system described in AN ALGEBRAIC LANGUAGE FOR THE MANIPULATION OF SYMBOLIC EXPRESSIONS. Basically any time we say that an object has properties, we are referring to that.
Dynamically typed. As a dynamic language Clojure is less capable than many other Lisp implementations/languages. See for example 'late binding'. In Clojure you either need to define a function before its use or need declare it. Not that 'dynamic'. In a typical Lisp dialect the order of definitions makes much less a difference, since functions can be called late-bound. For an interpreter-based implementation, it makes no difference at all.
> It promotes combinations of built-in data structures (lists, maps, vectors) over objects
An aspect I don't like. This makes debugging of larger systems more difficult, since maps are maps. Whereas objects have a type tag built in, have a list of allowed fields, etc. etc. They simply have more more explicit and standardized structure to them.
> Some built-in features like reducers and transducers rely heavily on composing higher order functions for transforming other functions.
Which makes code complex, since transducers are a mildly complex mechanism.
> It encourages REPL-based development
In relatively weak form. Basic features of Lisp need to be added to do simple tasks: like 'instrumenting' code to debug it, since it lacks an interpreter or more capable compiler support. REPLs with actual interactive error handling are not standard. Interactive Lisp compilers like the one from SBCL give much more information and warnings.
Thus the Lisp development features are at best only medium-level.
> Historically, Lisp programmers weren't the biggest proponents of OOP
Funky, people from the Lisp community helped to shape OOP and explored much of it. From guys like Hewitt who developed the actors paradigm in the early 70s, Minsky who developed the frame theory mid 70s and inspired a whole range of OO programming in Lisp, to Howard Cannon who developed Flavors in 1979 with flexible mixins and multiple inheritance used to implement the object-oriented operating system parts of the MIT Lisp Machine, to Lieberman who developed OOP with prototypes mid 80s, to Kiczales who worked a decade on OO in Lisp (LOOPS, CLOS, Meta-object Programming, Object-oriented Protocols, Meta-level architectures, early Aspect oriented programming, ...).
Actually objects in the early Lisp were simply symbols with their property lists. These property lists could hold both normal attributes and also functions.
reply