I believe they are right. Flexible systems must be more general than their application. Otherwise the flexible system is not an abstraction, it's just a collection of code.
Clojure is a flexible language
In a discussion with a friend who enjoys programming greatly, I said "I don't really care about syntax". That statement surprised him. Frankly, the statement surprised me too. Syntax isn't irrelevant. But it's not the goal. Some programming languages depend greatly on the syntax. Others don't.
Clojure establishes syntax for sequential data, associative data, sets, function calls and macro calls.
(nssteel-beams-si-units-clojure-multimethods
(:refer-clojure:exclude [* / + -])
(:require
[clojure.java.io :as io]
[clojure.math :as math]
[clojure.set :as set]
[clojure.string :as str]
[nextjournal.clerk :as clerk]
[taoensso.nippy :as nippy]))
Examples
[123]
⇒
[123]
{:name"Teodor",
:thesis
{:title
"Finite element implementation of lower-order strain gradient plasticity in Abaqus",
If you accept these five decisions about syntax, you can mostly do whatever you want afterwards.
You can define your own functions, and your own macros. last is clojure.core/last, set/union is clojure.set/union. Let is clojure.core/let, a macro. The only reason you can write let and have that automatically refer to clojure.core/let is that all vars from clojure.core are required by default (which can be disabled).
What about syntax for function definitions? Syntax for type definitions? Syntax for loop constructs? Where are they?
There is no syntax for function definition — fn is a macro.
There is no syntax for type definitions — you can choose a library for checking data (like clojure spec or malli, if you want. If you do need types, there's deftype, defrecord and defprotocol, all three are macros.
There is no syntax for for loops — for is a plain macro
There is no syntax for while loops — loop and reduce are macros.
This is good!
Take the time to learn and understand vectors (sequential data), maps (associative data), sets (unique elements only) and functions. Idiomatic Clojure code is functions transforming data. Leave the rest for when you actually need it!
Steel beams
Wait, what about the steel beams? I thought there was supposed to be steel beams.
Steel beams coming right up!
Flexible languages can solve a wide variety of problems. A problem you might pick for yourself is to design steel beams. Let's see if Clojure is a good fit.
This is a steel beam:
Steel beam supporting the first floor of a house
Image from Wikipedia, retreived 2023-10-28, licensed CC BY-SA 3.0.
This a figure of steel beam's cross section:
Cross section of an I-shape beam
Image from Wikipedia, retreived 2023-10-28, licensed CC BY-SA 3.0.
Let's make a similar figure ourselves. We will represent beams as maps.
]}{:namenextjournal.clerk.viewer/read+inspect-viewer:render-fn(fn[x](try2 more elided))}{6 more elided}{6 more elided}{6 more elided}{5 more elided}{4 more elided}{4 more elided}{6 more elided}{4 more elided}22 more elided]
A viewer is a map. What keys are used by other viewers?
Note: since we're using defrecord to implement our SI units, we will inherit Clojure's value-based equality. That's not what we want! Here's an example:
(=
(WithUnit.0.3 {:si/m1})
(WithUnit.0.3 {:si/m1:si/s0}))
false
Our problem is zero exponents in the exponent map. We can fix this with a contructor that conforms units to the representation we want.
But what units do :r, :wy and :iz have? Let's make a new map where values have SI units.
Meters, millimeters and square millimeters are convenient to define with multiplication:
(let [m (with-unit1 {:si/m1})
mm (*10e-3 m)
mm2 (* mm mm)]
(clerk/row m mm mm2))
Loading...
Loading...
Loading...
But for Loading... and Loading... using multiplication is annoying. Let's fix that by defining exponentiation for WithUnit.
The implementation is quite similar to multiply, except that we don't allow numbers with units as exponents. We'll rely on clojure.math/pow under the hood.
(do
(defmulti pow both-types)
(defmethod pow [Number Number]
[base exponent]
(math/pow base exponent))
(defmethod pow [Number WithUnit]
[base exponent]
(throw (ex-info"WithUnit as exponent is not supported"
{:base base :exponent exponent})))
(defmethod pow [WithUnit Number]
[base exponent]
(with-unit
(math/pow (.number base) exponent)
(update-vals (.unit base) (partial * exponent))))
(defmethod pow [WithUnit WithUnit]
[base exponent]
(throw (ex-info"WithUnit as exponent is not supported"
{:base base :exponent exponent}))))
#object[clojure.lang.MultiFn0x752a6b5a"
clojure.lang.MultiFn@752a6b5a"
]
Does it work as expected?
(let [m (with-unit1 {:si/m1})
mm (*10e-3 m)]
(clerk/example mm
(* mm mm)
(pow mm 2)
(= (* mm mm) (pow mm 2))))
Examples
mm
⇒
Loading...
(* mm mm)
⇒
Loading...
(pow mm 2)
⇒
Loading...
(= (* mm mm) (pow mm 2))
⇒
true
Looks all right to me!
Time to revisit our IPE beam. This time, we add units.
:profile300:r Loading... :s Loading... :t Loading... 2 more elided}
Thank you
To Sam Ritchie, Martin Kavalar and Jack Rusher for making good tools, and for helping people who want to learn. To Gerald Jay Sussman for improving the way we think about programming, and expanding the range of problems we can solve with programming. To Eugene Pakhomov, Joshua Suskalo and Ethan McCue for helping me understand how Java types, Clojure multimethod type hierarchies are connected.
To see how a real-world flexible system is built, explore Emmy's architecture. Emmy is inspired by Gerald Jay Sussman's scmutils, and I'm strongly guessing experience building scmutils informed the writing of Software Design for Flexibility.