

What is simplicity in programming and why does it matter?
source link: https://blog.jakubholy.net/2021/simplicity/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Examples
Let’s have a look at a few examples of simplicity in practice to get a better grasp on what it actually means.
Unix tools
The Unix tools philosophy is based on simplicity:
The tools philosophy was to have small programs to accomplish a particular task instead of trying to develop large monolithic programs to do a large number of tasks. To accomplish more complex tasks, tools would simply be connected together, using pipes.
So you have small, single-purpose elements that share a simple, well-defined, generic interface (lines of text) and thus you can combine them in many ways to achieve many different goals. You can read an access log with cat
, filter only the requests from a particular IP with grep
, extract just the response code with cut
, sort it with sort
, and get all the unique values with uniq
. (The |
interface is great for many purposes but actually too simple for others, which prompted the creation of the Elvish Shell, whose pipes can carry structured data.)
Clojure
Just a few examples.
Clojure gives you many of the same tools that you get in an OO language such as Java but contrary to these, you get each of them as a separate thing your are free to use and combine with others as you see fit. Java tangles polymorphism with hierarchy and code sharing (via inheritance). Java polymorphism dispatches dynamically based on the type of the target object and the inheritance hierarchy thereof. Clojure has two forms of polymorphism, the simpler protocols and the more powerful multimethods. Multimethods give you the same possibilities as Java, but separately. Again you dispatch based on a value - though here it is an arbitrary value computed from the function’s arguments, not just the type of the first argument. Typically these dispatch values are disjoint but if there is an overlap (as between :animal
and 🐈) then you can define an arbitrary hierarchy of these values using Clojure’s derive
. (I’ve never needed that because my needs were simple enough. Clojure allows me to use correspondingly simpler concepts and write simple code while Java forces me to always use its more complex concepts because it lacks any simpler ones.)
In Java, implementing an interface requires that you have control over the target class. That is an unnecessary and occasionally painful limitation. Clojure has protocols, which are quite similar to interfaces, but you can implement them for a class you do not control. Thus polymorphism in Clojure is independent from code ownership (to an extent). That is very powerful and useful.
A key source of simplicity in Clojure is that all data is represented by a few generic data structures and the core library provides tens of powerful functions to work with these and a few abstractions above them. In Java, everything has its own classes with their own, unique methods - effectively custom entity-specific languages. The ≈ 100 Clojure collection sequence functions I get to use again and again, with every library and framework I ever use. And similarly I reuse the few key higher-order functions to express powerful transformations. In Java, I have to learn a new "API" for each class and write tons of bespoke glue code to get data to flow from one to another. I have ranted about this before in Clojure vs Java: The benefit of Few Data Structures, Many Functions over Many Unique Classes where I had to deal with data flowing from HttpServletRequest
→ Apache HttpUriRequest
and Apache HttpResponse
→ HttpServletResponse
. What does this have to do with simplicity? Clojure only has a few parts, i.e. the 4 core collections, contrary to Java’s infinite number of data representations and data access forms. And it empowers you to process them with generic functions that are oblivious to the concrete domain - orthogonal to it - while in Java you are forced to write code that is much, much more case-specific.
EDN > JSON, EQL > GraphQL
Extensible Data Notation (EDN) is a Clojure parallel to JSON but it supports more data types (such as symbols, keywords, sets) and key types and, most importantly, it is extensible through "tagged literals", with some extensions included out of the box such as for dates (ex.: #inst "2021-06-13
), which JSON is sorely missing, and regular expressions (#"^Hello*"
).
GraphQL is a graph data query language with a unique syntax. It is typically embedded in JavaScript as a string. Its Clojure parallel is EDN Query Language. The big difference is that EQL is expressed using ordinary EDN data structures - because EDN (contrary to JSON) is powerful enough. And thus, contrary to GraphQL, you do not need any special APIs to parse, transform, or programmatically generate these queries. You can simply use the old, good Clojure functions you already use million times a day. You do not need anything special to define fragments - just use data and functions. And your editor can already highlight the syntax of the queries.
We have a simple but powerful (and extensible) building block - EDN - that is able to satisfy many unexpected needs, such as EQL, while the JavaScript folks needed to invent a whole new language. (A key advantage of EDN here is that it has lists and symbols so that it can already represent mutations and parametrized queries and distinguish them from the data need definitions, as demonstrated here: [(delete-user 123)]
or [({:all-employees [:name :age]} {:page-nr 3, :page-size 10})]
. JSON is too limited with strings, {}
, and []
.)
Stu Halloway’s Reflect utility
Once upon time, Stu Halloway wanted to add a utility to Clojure to make it easier to explore Java objects, as described e.g. in Simplicity Ain’t Easy. This was his second attempt:
user=> (describe String ;
(named "last")) ;
=========================================
class: java.lang.String
Filters: [(named last)]
=========================================
int lastIndexOf(java.lang.String) public
int lastIndexOf(int) public
...
describe
prints information about a Java class and its methods2We can also filter the methods shown using convenient, clearly named predicates it providesThis solutions is not good enough - it is not simple enough. Here is the final variant:
user=>(require '[clojure.reflect :refer [reflect]]
'[clojure.pprint :refer [print-table]])
user=> (->> (reflect String) ;
:members ;
(filter #(.startsWith (str (:name %)) "last")) ;
(clojure.pprint/print-table)) ;
| :name | ... | :parameter-types | ...
|-------------+-----+---------------------------------...
| lastIndexOf | ... | [java.lang.String] | ...
| lastIndexOf | ... | [int] | ...
| ...
The final solution is far less "pretty" - it requires more code, you need to learn the shape of the data, and are forced to put all the pieces together manually. But it is also far simpler - each part does a single thing and is independent from the others:
reflect
leverages the ubiquitous, generic Clojure exchange interface - data; we also use a generic Clojure connection facility to connect the pieces together - the threading macro->>
There is no custom API for data access - use Clojure’s general data access to get the subset of data you want
There is no custom API for filtering - use Clojure’s general data access sequence functions to filter and process the data in any way you want
Printing isn’t baked into the API anymore. Instead, there is a general purpose printing function that can print any data
What are the pros and cons?
The disadvantage is that it requires more work from you - you need to learn about the pieces (reflect
, print-table
) and understand the data they work with. Filtering is more verbose. On the other hand, you can add convenience wrappers of your own, ones that fit your needs 100%, should you desire so - because now you can.
The advantage is that you get a generic printing function that you can use for any, completely unrelated, needs and that you get the information about the class as data. Data that you can process leveraging the multitude of functions and libraries you already know and to purposes other than printing, purposes the author did not expect. And you do not need to learn any new "entity specific language" of method filtering predicates.
Recommend
-
11
What is HTTP/3, and Why Does it Matter?HTTP/3, or HTTP over QUIC, brings many new performance features to HTTPPhoto by JJ Yi...
-
12
The scaling of Ethereum – or lack thereof – remains a pressing problem in the blockchain industry. Even though ETH 2.0 will offer m...
-
6
What is a Syllabus and why does it matter? Over in the Facebook CS Ed groups there was a discussion of the forthcoming code.org APCS-A curriculum. As far as I can tell, the curriculum isn't...
-
4
What is Web3.0 and why does it matter? Becky | May 31, 2022
-
6
What is longtermism and why does it matter? By William MacAskill8th August 2022
-
8
What is Demand Generation and Why Does it Matter?
-
15
What Is Branding and Why Does It Matter? ByBrooke Arnold PublishedDecember 12, 2022December 12, 2022 Vandelay Design may receive compensat...
-
3
-
9
Apple @ Work: Why Wi-Fi 6E solves a major problem for IT professionals supporting remote employees
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK