https://news.ycombinator.com/item?id=16414942
This is all application specific, but for the types of apps I've worked on (large enterprisey OO apps) you often need various bits and pieces of domain data across different methods. So given some function, you either pass in DomainClass1, DomainClass2, DomainClass 3 (using a couple of properties of each) or you define a new class SomeSubsetOfPropertiesClass solely to call that single method. In the former case, the types do not serve as documentation for the reader as it's not clear what shape of the data is required for the function. In the latter case you're duplicating code (the properties and their types) and the class really has no meaning except as a struct to call that method.
Now that I've been working with Clojure for a little bit I find I'm able to write much more concise, testable functions and calling them is dead simple since I can work with the raw data, transforming it into the shape I need.
An example in TypeScript:
interface Named {
name: string
}
class Person {
name: string
age: number
// constructor here
}
function f(obj: Named) {
// do something with obj.name
}
const joe = new Person('Joe', 25)
// Compiles, even though Person has an extra `age` field
// Person is structurally compatible with Named
f(joe)
If you pass f() a class/object that doesn't have a `name` property of type string, the compiler would catch your error.If I change the type of "Named" to have the fields `firstName` and `lastName` of type string, accessing any other property inside `f` (like `obj.nonExisting`) or passing objects that don't have those fields.
Even better than that, you don't need to use classes, you can use "normal" JS data structures. Extending the last example:
const john = {
name: 'John',
age: 25
} // No class involved
f(john) // compiles
const alice = {
age: 25
firstName: 'Alice',
lastName: 'Jones'
}
f(alice) // compile-time error
Clojure.spec is cool, but I don't see how that is incompatible with static typing. You can still have libraries that check more complex properties at runtime.EDIT: > Also, Clojure.spec allows you to be much more precise about properties. For example, it must be [...] non-nil [...]
TypeScript also handles nulls in the type system:
function f(x: string | null) {
if (x != null) {
// tsc knows that inside this if, x can't be null
return x.length
} else {
console.log(x.length) // this doesn't compile, x is of type null here
}
} (def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$")
(s/def ::email-type (s/and string? #(re-matches email-regex %)))
(s/def ::acctid int?)
(s/def ::first-name string?)
(s/def ::last-name string?)
(s/def ::email ::email-type)
Using those keywords you can define maps which specify shapes of data: (s/def ::person (s/keys :req [::first-name ::last-name ::email]
:opt [::phone]))
Functions have their own separate specifications. Here's one that accepts a person and an acctid: (s/fdef add-to-account
:args (s/cat :person ::person :acctid ::acctid)
And must be called like this: (add-to-account {::first-name "John" ::last-name "Smith" ::email "john@smith.com"} 12345)
If you tried calling it with an illegal argument and it's instrumented you will see an error: (add-to-account {::first-name "John" ::last-name "Smith" ::email "abc123"} 12345)
ExceptionInfo Call to #'scratch.core/add-to-account did not conform to spec:
In: [0 :scratch.core/email] val: "abc123" fails spec: :scratch.core/email-type at:
[:args :person :scratch.core/email] predicate: (re-matches email-regex %)
Here's the difference. What if I have another function that just accepts an email: (s/fdef lookup-user
:args (s/cat :email ::email)
And another which looks up by last name: (s/fdef lookup-user-by-name
:args (s/cat :last-name ::last-name)
And now imagine if you wanted to accept either an email or last-name (notice it does not match our person spec). Attempting to use interfaces would quickly get out of control. You'd have to create an interface for every single property and extend interfaces to form arbitrary groups of properties.This is what allows reuse.
- The vast core library of functions that manipulate those data structures can be used for everything in your program, cos it's all data.
- Most clojure libraries take and/or return data, reducing the need for clumsy adaptors, or even worse not being able to get at the data you need cos the library writer was really enthusiastic about encapsulation of everything they thought was of no use to consumers.
- You don't have a person class, you have a map with a first name and last name. Now the function that turns first + last name into full name can be reused for any other map with the same keys. (A rather spurious example, but a real one would take a large codebase and an essay to describe)
I can only recommend watching some of Rich Hickey's talks, particularly these ones, they're not entirely about types, but they express the above ideas much better than I can:
- Simple made easy https://www.infoq.com/presentations/Simple-Made-Easy
- Effective programs https://www.youtube.com/watch?v=2V1FtfBDsLU
- Are we there yet? (this one is more about OOP, but unless you're using something like haskell, idris etc its relevant for your type system of choice) https://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hi...
What about this can't be done with types? Simple parametric-polymorphism gets you pretty far. Row types allow you to handle "maps as records" in a type-safe way. The rest is just having support for some kind of ad-hoc polymorphism so that you can re-use your functions on that small set of types (type classes, ML-style functors, interfaces, protocols, etc.).
I'm familiar with the advantages of type systems (my progression was Java -> Haskell -> Idris) but I found my personal productivity (even in larger systems built in a team) was best in clojure. I didn't feel that the guarantees given to me by the type system were worth the mental overhead, a lot of people feel differently (you amongst them I'm guessing :p)
As a closing point, if I were to ever build something that truly had to be Robust in a "someone will die if this goes even slightly wrong" way, I would reach straight for Idris and probably something like TLA+. However most of my development revolves around larger distributed systems communicating over wires, still resilient but in a different way. Mainly I use clojure.spec in core business logic and at the edges of my programs, for generative testing and ensuring that the data flowing through the system is sensible.
Also I personally find that to be too much overhead and ceremony in return for some type checking at compile type, as opposed to spec checking at runtime.
What do you mean by "data-oriented language"?
In the same way you can do immutable and functional stuff in java, it's not going to mesh with the rest of the ecosystem or language around you.
Clojure has classes and types. How can it be untyped?
Machine language is untyped.