Skip to content

Instantly share code, notes, and snippets.

@D00mch
Last active December 2, 2023 15:42
Show Gist options
  • Save D00mch/d5a85cc2037e55ca0b49466b76a8ebdd to your computer and use it in GitHub Desktop.
Save D00mch/d5a85cc2037e55ca0b49466b76a8ebdd to your computer and use it in GitHub Desktop.

Why Flutter needs Clojure

”If you like Java, program in Java. If you like C#, program in C#. If you like Ruby, Swift, Dart, Elixir, Elm, C++, Python, or even C; by all means use those languages. But whatever you do, learn Clojure, and learn it well.” --- Uncle Bob (and several more tweets: 1, 2, 3).

There are lots of articles about Clojure. In this one, I'd like to share my thoughts on the advantages of using it for cross-platform mobile development. Dart programmers are the target audience, but anyone interested in Flutter and/or Clojure is welcome.

A short history of Clojure

Clojure was not created in a hurry (JS), it didn't try to seize the market with a multimillion budget (Java). It wasn't backed by a powerful company (Go, Dart, Kotlin), and it wasn't the only platform's language (Swift, C#).

So why did Clojure happen to be noticed? Why is it the most paid language and one of the three most loved? Why do people write ports for Java, JS, C#, Unity, Python, Elm and, finally, Flutter? (proofs in FAQ below)

The answer could be found in Rich Hickey's "Simple Made easy" talk. My unforgiving short and shameless version: the pal thought on how to make good stuff, directed by principles of simplicity, stability, and practicality, and made Clojure. He did not depend on deadlines, budgets, or the pursuit of hype and vogue.

Clojure basics

You should first be aware that an expression in Clojure is represented as a list, with the first element being a function and the other elements being arguments, in order to make it easier to read.

(fn arg-1 arg-2 ... arg-n)

Each argument and even a function could be the same kind of expression, for example:

(fn-2 (fn-3 arg) arg-2)

At first, the (fn-3 arg) expression gets evaluated:

(fn-2 evaluated-arg arg-2)

Although it is simplified, it is sufficient to comprehend the examples I will provide.

Syntax simplicity and consistency

Examples in Dart and Clojure doing the same:

  • Dart: max(1, 2);

  • Clojure: (max 1 2)

  • Dart: a > b || (c > d && d > e && e >f);

  • Clojure: (or (> a b) (> c d e f))

  • Dart: if (a == 1) a else b;

  • Clojure: (if (= a 1) a b)

  • Dart: int square(int n) => n * n;

  • Clojure: (defn square [n] (* n n))

Note that in each case where parentheses are used in Dart, they signify different things, such as calling a function, controlling the order in which statements are executed, capturing a special form of if, or grouping function parameters.

Contrarily, in Clojure parentheses are always used to combine a function or a macro with arguments.

In addition, it is not immediately obvious on Dart (you must be aware of or use parentheses), which operator (<, ||, or &&) will be executed first. The parentheses in Clojure indicate everything.

  • Dart: ++i;
  • Clojure: (inc i)

One more thing. Dart uses infix (1 < 2), prefix (max(1, 2)), and postfix (i++) notations, whereas Clojure has only prefix notation.

Since Clojure is more consistent and has fewer rules, its syntax is typically much simpler than Dart's. Check the Antlr parsers (and lexers) for Clojure and Dart for the simplest way to demonstrate this difference.

The descriptions of Clojure use five times fewer words in the links I've provided. There will be a 9-times difference if we use the official Dart parser (spec).

And here is the Tree-sitter grammar: Clojure has 1673 lines and Dart --- 10462.

Concise and elegant Dart Clojure

Take a look at this example with the same code in Dart and Clojure side by side. Another is this cookbook implemented in Clojure with 50 percent fewer lines of code.

To demonstrate why code in Clojure is typically more concise, I'll give several examples in this section of the article.

Nesting is defeated with six lines of code

In Dart, nested widget resemble a ladder.:

Container(
  color: Colors.red,
  child: const SizedBox(
    child: Center(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Text(
          'Oh my god, how “clutter” is it!',
        ),
      ),
    ),
  ),
);

Why shouldn't we eliminate this duplication if we are aware that each nested widget has a ":child" argument? Like:

nest(
  Container(color: Colors.red),
  SizedBox(),
  Center(),
  Padding(padding: EdgeInsets.all(16)),
  Text('Oh my god, how “flutter” is it!'));

Since the :child parameter is a required one, there is no way to solve this during compile time (as I wrote it above or with annotations). We could ask dart compiler developers to make the nest, but will they agree to add a new keyword (no)?

For a limited number of widget classes, we could use the reflection API to address this in runtime (although since :child fields are final, we couldn't modify them; we would have to copy instead). The drawbacks include performance and the amount of code required.

Because we are unsure of the widget implementation that the user will supply to our nest function, it is also impossible to solve this problem in runtime for a general case. Perhaps there is a widget with a :child named parameter, but the user saves it as a field with a different name inside the implementation. How do we find this name?

Regarding Clojure, the original code appears as follows:

(Container 
 :color m.Colors/red
 :child 
 (SizedBox 
  :child
  (Center 
   :child 
   (Padding 
    :padding (EdgeInsets/all 16)
    :child (Text "Oh my god, how “clutter” is it")))))

However, we can simply rewrite it:

(nest
  (Container :color m.Colors/red)
  (SizedBox)
  (Center)
  (Padding :padding (EdgeInsets/all 16))
  (Text "Oh my god, how “flutter” is it"))

It will work in a general case and resolve all the aforementioned issues.

How is such an unfair power even possible?

Code as data, or homoiconicity. You can put a program code into a data structure and evaluate it. For example, code written in Dart could not be put into Dart's array, but we can do this in Clojure.

If the language is homoiconic, then you can easily manipulate the syntax tree: write and modify a Clojure code with Clojure, that is, expand the compiler. The user's tool is macros, and the following is an example of a macro that allows you to "straighten" the nesting:

(defmacro nest [form & forms]
  (let [[form & forms] (reverse (cons form forms))]
    `(->> ~form ~@(for [form forms] 
                    (-> form 
                        (cond-> (symbol? form) list) 
                        (concat  [:child]) 
                        (with-meta (meta form)))))))

We don't have to write this as it's already exist (nest). And the main widget macro supports it.

Magic apply

Thanks to the apply function, we can do things that are not possible in other languages. Let's go over the examples first, and then we'll examine how it works.

How to check that collection is sorted?

In Dart:

bool isSorted(List<int> list) {
  if (list.length < 2) return true;
  int prev = list.first;
  for (var i = 1; i < list.length; i++) {
    int next = list[i];
    if (prev > next) return false;
    prev = next;
  }
  return true;
}

isSorted([0, 1, 2, 3, 4, 5]); // true

var list = [for(var i=0; i<6; i+=1) i];
isSorted(list); // true

In Clojure:

(apply < [0 1 2 3 4 5]) ;;=> true
(apply < (range 6))     ;;=> true

How does it work? applу accepts a function and a list, then passes the list's items as arguments. As a result, the expression becomes (< 1 2 3 4 5). This is a slightly unfair example as Clojure's > function accepts any number of arguments.

Let's glance at another, more intricate example.

How to transpose a matrix?

In Dart:

List<List<int>> transposeList<R>(List<List<int>> input) {
  List<List<int>> output = [];

  for (int i = 0; i < input[0].length; i++) {
    output.add(List<int>.generate(input.length, (idx) => idx));
  }

  for (int i = 0; i < input.length; i++) {
    List<int> column = input[i];
    for (int j = 0; j < input[0].length; j++) {
      int rowItem = column[j];
      output.elementAt(j).removeAt(i);
      output.elementAt(j).insert(i, rowItem);
    }
  }

  return output;
}

transposeList([[1, 2], [3, 4], [5, 6]]); // [[1, 3, 5], [2, 4, 6]]

In Clojure:

;; function declaration, without external libraries
(defn transpose [m] (apply map list m))

;; function invocation
(transpose [[1 2]
            [3 4]    ;; => [[1 3 5]
            [5 6]])  ;;     [2 4 6]]

How does it work? The map function expects a function and one or several sequences. Examples:

(map inc [1 2 3])     ;;=> (2 3 4)
(map odd? [1 2 3])    ;;=> (true false true)
(map + [1 2] [1 1])   ;;=> (2 3)
(map max [1 2] [2 1]) ;;=> (2 2)

As a result, the expression (apply map list [[1 2] [3 4] [5 6]]) becomes:

(map list [1 2] [3 4] [5 6])

And then evaluates to:

[(list 1 3 5) (list 2 4 6)] ;;=> [(1 3 5) (2 4 6)]

Collections work as they should

My point goes beyond the simple fact that in Clojure functions like equals, compare, sort, and others just work (unlike Dart, where you must use a Collection library to compare two lists).

Clojure's core collections are persistent, which means they are immutable and can be copied almost instantly (Log32). For example, a function that adds a key to a map would return a new map without changing the original. And it's effectively in constant time.

Because memory is reused internally (only the difference counts, per wiki), you can have many maps for the same memory cost as one; which makes it possible to write effective, thread-safe code.

Clojure and data oriented programming

To put it simply, a data-oriented approach separates code from data. In doing so, we separate the data hierarchy from the logic hierarchy (classes), which results in a lower coupling. Following are some examples from the related article:

Without separation: complex-class-relation1.png

With separation: data-code-relation2.png

The subject is huge and won't fit into one article, so look into the advantages of this approach in the book and the blog. I would also recommend this video with OOP-experts code simplification. I will only discuss one aspect here.

The boilerplate code overriding hash, ==, toMap, fromMap, toString, copyWith is not necessary when representing domain models as data structures, such as maps. This would merely be one of many helpful functions to work with Clojure collections that are readily available.

To illustrate this, I want you to picture a representation of a "human" where we want to increase an age.

(def human {:name "Bob", :age 30})
(def old-human (update human :age inc)) ;=> {:name "Bob", :age 31}

In OOP, a Human class could have an increaseAge method. What if, however, we wanted to be able to decrease our age? In Clojure, we'll simply pass dec (instead of inc), but what about Dart? Add another method?

The issue with this approach is not just that we have to add more code, but also that we are constantly dealing with new classes that have their own methods. This can be seen by contrasting the Dart (Java, Kotlin, Swift) and Clojure experiences when trying out new libraries. Clojure ones are easier to work with, at least due to the fact that you do not need to learn new methods/funcitons.

The same in other words: (from Clojure website): Putting information in such classes is a problem, much like having every book being written in a different language would be a problem. You can no longer take a generic approach to information processing. This results in an explosion of needless specificity, and a dearth of reuse.

You can manipulate the data of any library, as if you had written these methods yourself, thanks to the vast array of functions I previously mentioned. That's what Alan Perlis meant when he said:

"It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures".

Retention and stability

I've been developing for Android for seven years, and during that time, a lot has changed only in the specific fields of concurrency and "asynchronousy."

People initially used raw Threads, IntentServices, and AsyncTasks. Then RxJava emerged, articles and books were written, lots of libraries supported it. I remember having argument with my teamlead about using RxJava2. He refused and proved to be right as RxJava3 appeared, but Kotlin Coroutines was the new trend.

WorkManager came to replace JobScheduler, that replaced AlarmManager (in tandem with BroadcastReceiver and Service). Who will replace WorkManager? Lets wait for a year or two and find out.

In the world of Clojure, everything is different. Core.async is still in use today, nine years after it was developed. In general, Clojure code is extremely durable. Here is a link on "A History of Clojure" by Rich Hickey, where (page 26, or this twit) you can compare Scala and Clojure in terms of code retention.

One language for everything

At last, Clojure could be used not only for a Flutter development. Below is the list of all the possible applications that I consider practical:

C++ host jank is upcoming.

In my most recent project Dart to Clojure translator, for example, the core, dependencies, deploy, and build scripts are all written in the same language. The application contains a jar file, a native image (Graalvm), npm (ClojureScript), as well as Java and JS libraries.

FAQ

How do you prove that Clojure is as good as described above?

Aside from the fact that Clojure is the third most loved language according to the StackOverflow survey, and my own experience --- which I try to keep separate from --- I can think of a number of other examples to support this claim.

Here is the reproduction research on the impact of programming languages on code quality. In essence, commits that fixed bugs were examined in GitHub projects.

Some highlights:

  • Languages associated with fewer bugs were TypeScript, Clojure, Haskell, Ruby, and Scala, while C, C++, Objective-C, JavaScript, PHP and Python were associated with more bugs.
  • Two extreme cases: C++ (most bugs), and Clojure (least bugs).

Libraries' sizes and salaries (next 2 questions) are two additional potential proofs.

Are Clojure code bases typically smaller (compared to other languages)?

I don't know of any scientific research, but there are enthusiastic comparisons, like this one, where Clojure is the 2nd among 24 other frameworks. In this "Love Letter To Clojure" Gene Kim rewrote his React/Js app (1500 lines) to ClojureScript (500 lines).

How much money do Clojure developers make?

According to StackOverflow survey in 2022 (and 2021 as well), Clojure developers are paid the most. I came across a Reddit post that provided some justifications for the idea that the language attracts seasoned (and expensive) developers, which could be supported by the same survey (link). The thesis might also be backed up by State of Clojure 2022, question 8. More than 76% of developers have experience of at least six years, and 50% have more than eleven years.

Why isn't Lisp more well-known if it's such an effective dialect?

I think we should take two things into account. The first is that popularity and quality are not necessarily correlated. The most popular language is JS, for instance.

Another thing is The Lisp Curse. It's assumed that Lisp allows one to be extremely productive. So, instead of relying on companies to achieve results, the developer does it himself/herself.

"Most of these projects will be lone-wolf operations. Thus, they will have eighty percent of the features that most people need (a different eighty percent in each case). They will be poorly documented. They will not be portable across Lisp systems."

For instance, 75% of Java engineers use IntelliJ IDEA as an editor (2021), while in Clojure there is no one most popular editor and distribution is somewhat even (Emacs, Idea, VSCode, Vim and less popular Atom, Sublime, NightCode). Same with web frameworks.

It is believed, however, that Clojure partially escapes the LISP curse by making use of the well-maintained host libraries Java/Js/Dart/C# (since Clojure is a hosted language).

Why is functional programming less common than OOP?

It's difficult to give a simple answer to this question, but keep in mind that software development has marketing-related aspects. More than 500 million dollars were invested in marketing campaigns for Java. Why does Java support the OOP paradigm? Maybe it was easier to sell this to C++ developers.

If you are interested in this subject, I suggest watching Why Isn't Functional Programming the Norm? video.

<iframe width="700" height="389" src="https://www.youtube.com/embed/QyJZzq0v7Z4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay=0; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

Shouldn't one be afraid of dynamic typing?

My impression is that different people "despise" dynamic typing for different reasons.

If your experience with JS has made you dislike dynamic typing, you might actually dislike implicit weak typing. But unlike JS, Clojure does not have the odd problems like listed below.

1 === '1';    // false
1 == '1';     // true
true === 1;   // false
true == 1;    // true
[0] == 0      // true 
{} + [] == 0  // true`

If you were not pleasantly impressed by Python, you probably had two problems with it: performance and large code base maintenance (difficult to understand others' code without types).

When using Clojure, you won't encounter performance problems (for the most cases) because your functions are transformed into hosted language methods; if reflection is required, you can use type hints to avoid it. Maintenance issues are also not a problem; in my opinion, REPL is a game-changer here (it's not supported for ClojureDart yet).

If you conceptually disagree with dynamic typing and don't see any benefits to using it, it might make sense to look into Gödel's incompleteness theorems regarding the drawbacks of formal systems (or should we say typed systems?).

Maybe not is a great talk about the topic.

How to start with ClojureDart?

Many options are available, and it depends.

If you are unfamiliar with Clojure, you could attempt to get up to speed quickly by Learning X in Y minutes and completing a number of tasks from the 4clojure website. Another choice is Practical.li, which offers both written and visual content. And if you are into books, lots of people started with BraveClojure.

If you are already comfortable with Clojure and want to write Flutter applications right away, visit the ClojureDart page and check out the quick start guide. You could also check How to Start ClojureDart which is a piece I wrote on the topic.

A ClojureDart workshop or a ClojureDart YouTube channel are other options.

And if it has suddenly occurred to you that Clojure is a highly sophisticated technology that necessitates extensive knowledge of mathematics, this is unquestionably untrue. Since the language is pragmatic, one can avoid thinking about or even being aware of category theory entirely. Although reading difficult books like SICP is useful, it is not required.

What was left out

Read Evaluate Print Loop. In Clojure, the REPL connects to the application, allowing you to edit the program's code while it is still in progress without losing any state. For instance, you could connect to the handler on the backend in production, see what information is passed through it, and fix a critical bug there without having to re-deploy. REPL is integrated into the workflow (both in the editor and in the application).

This is not supported on consoles, replicas, or shells by Dart, JS, Python, Swift, Kotlin, Java, Scala, or Haskell. Their shells don't integrate with the program; instead, they resemble a console or a debugger that can be used to check the functionality of some pieces of code.

ClojureDart does not yet support REPL, but it soon will.

Summary

What makes Clojure a good choice for Flutter development? Basically, to acquire tools that increase development simplicity and speed.

Homoiconicity to extend the language

We can extend the language whatever we like thanks to macros. For instance, to make the code base smaller and increase readability (this article discusses the example with "nest," which removes widget nesting).

Persistent data structures to simplify working with data

By mastering the basics of working with data in Clojure, you can advance to the point where any project or library's code is predictable and understandable, as everyone uses the same approach to modeling data.

Additionally, writing immutable code is easier since you may copy collections with O(Log32).

And more

Functional programming. Reliable libraries. Elegant and consistent syntax. Supporting community.

@madis
Copy link

madis commented Oct 17, 2023

💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment