An F# Dev's Perspective on Clojure

F# is a phenomenal programming language. And, it changed my life. No, really, it did.

Then, I joined Splash Financial in February 2021. They do Clojure, so I had to start learning it! And yes, we’re hiring! My Twitter is open to DMs too 😎.

Spoiler alert🚨: Clojure is really good.

I promised one of my F# friends and former co-workers, Matthew Crews, that I'd write up a comparison of the languages. So, here it is! As I was writing this, I realized that I like Clojure more than I thought. My nostalgia for F#, though warranted, may be inflated! Also, this topic deserves to be a blog post series or even a book, but I'm just over here writing my annual blog post 🤣.

This post may be a decent introduction to Clojure, but this video is my favorite:

(Note: Some of the jokes will go over your head if you haven't watched some Rich Hickey talks and started poking around in Clojure culture.)

Everyone approaches new programming languages with their previous ones in mind. I did old-school C in college - like a version where we had to declare i outside of for loops. Then, I did C# and F# professionally for 5 years. F# taught me to love functional programming, and Clojure is a very strong functional programing language.

BDFL

F#'s benevolent dictator for life (BDFL), Don Syme, is a wonderful human being. Thanks for all you do, Don! If having a BDFL over your programming language seems right, then you'll feel right at home in Clojure. Rich Hickey is also an awesome person and the BDFL of Clojure.

Syntax

I love F# and Clojure syntax. F# syntax is strongly influenced by its OCaml heritage, and Clojure pulls from its Lisp roots. Both languages add novel syntax. Both offer excellent interop with their main host runtimes - JavaScript for both, .NET for F#, and Java for Clojure. Note: There are more runtimes, but they're not nearly as significant, e.g. Clojure on .NET or F# on Python.

I was looking for this meme, but I couldn't find it, so I'll just recreate it in text.

(add 1 1)
"too many parentheses (Lisp family, e.g. Clojure)"

add 1 1
"too academic (ML family, e.g. F#)"

add(1, 1);
"just right (Algol family, e.g. C#)"

Pretty silly, right?

Anyway, I love not writing semicolons, so F# and Clojure both get a point there!

Let's look at some pairs of examples - F# first and Clojure second (the one with “all the parentheses”)!

Values

let greeting = "Hello, World!"

(def greeting "Hello, World!")

Not so different, right?

Functions

/// Function with no arguments.
let greet () = 
  "Hello, World!

(defn greet 
  "Function with no arguments."
  []
  "Hello, World!")

Pretty similar here too. Clojure functions use [] for function arguments. I really like how comments are inside the function definition instead of floating above.

Piping/Threading

let evenSquares =
    [0 .. 10]
    |> List.filter (fun x -> x % 2 = 0)
    |> List.map (fun x -> x * x)

I love F# list comprehensions! Wait, I’m supposed to be talking about pipelines.

(def even-squares
  (->> (range 0 11)
       (filter even?)
       (map (fn [x] (* x x)))))

In both languages, we’re threading a value forward to the last spot in an expression. F#’s pipe forward operator |> is needed in every expression. In Clojure, a single thread last macro, ->>, is sufficient no matter how many expressions follow. How terse!

Isn’t that even? function so handy?

Clojure has two more cool threading macros. Let's take a look!

;; thread first, ->
(-> "clojure"
    .toUpperCase
    .toCharArray
    first
    str)

Same as thread last, but for the first function parameter.

;; as->
(as-> [0 1 2] $
  (map str $)
  (clojure.string/join "" $))

Here, I bound the first expression to a symbol - I picked $. Then, that symbol is rebound to the result of each expression. Then, that symbol can be used in any position of the following expressions.

I find myself using thread last the most, probably because collection functions (map, filter, reduce, etc.) put the collection in the last position. Thread first, ->, tends to fit better in pipelines where the functions operate on a single value and then maybe accept some configuration parameters. The as-> macro is handy when things are a little jumbled. I use it the least. It's a fun little coincidence that -> has a single arrow head for single objects and ->> has multiple arrowheads for operating on collections.

F# Records and Clojure maps

F# records (and anonymous records) are one of the best and most basic features of the language. Let's remind ourselves what they look like.

// defining a record
type Person =  {Age: int; Name: string}
// creating an instance
let person = {Age = 5; Name = "Brett"}
// creating an instance with updated values
let olderPerson = 
    {person with 
        Age = person.Age + 1
        Name = "Brett, Sr."}

// creating an anonymous record
let anonymousPerson = {| Age = 5; Name = "Brett" |}
// creating a new record with a field added
let person2 = {| person with Location = "USA" |}

So, it's cool that you can update values and add fields, but you can't merge records or drop fields. With Clojure maps, you can! Maybe you'll be able to see in a small way why I make this terrible joke all the time when talking about Clojure: Lisp is the original list processing language. Clojure could have been called Mapp - the map processing language.

Coming from F#, I thought, I know what a map is! Map<K,V>. Easy! Nope. Clojure maps aren't homogenous - all the keys and values can have different types. Think of Clojure maps more like JSON, or dynamically typed Records. Clojure maps are key-value pairs of any type you want. One really neat thing about Clojure maps is keywords. They're like field names and look like this :some-keyword. I find that most maps use keywords for their keys.

;; create a map
(def person {:age 5 :name "Brett"})
;; => {:age 5, :name "Brett"}

;; add a key
(assoc person :location "USA")
;; => {:age 5, :name "Brett", :location "USA"}

;; update a key
(update person :age inc)
;; => {:age 6, :name "Brett"}

;; update multiple keys
(def somebody (assoc person :name "Brett, Sr." :location "USA"))
;; => {:age 5, :name "Brett, Sr.", :location "USA"}

;; remove keys
(dissoc somebody :age :location)
;; => {:name "Brett, Sr."}

;; merge as many maps as you want with increasing precedence (last one wins)
(merge
  {:name "Brett"}
  {:age 5 :location "USA"}
  {:age 6})
;; => {:name "Brett", :age 6, :location "USA"}

;; get a value by its key
(:name person)
;; => "Brett"

Isn't it amazing!?! Now I'm starting to understand why people want row-level polymorphism or whatever they're talking about in F#, or even just without. I hope it happens!

Project Files

In F#, we write project files in XML with names like MyProject.fsproj, like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>

In Clojure, we write adeps.edn. We didn't talk about EDN yet, but it's like the JSON of Clojure. It's amazing. Anyway, remember Clojure maps? A map is valid EDN. The minimal project file is an empty map:

{}

Here's a pretty normal example taken from clojure.org/guides/deps_and_cli:

{:deps
 {org.clojure/core.async {:mvn/version "1.3.610"}}

 :aliases
 {:test {:extra-paths ["test"]
         :extra-deps {io.github.cognitect-labs/test-runner
                      {:git/url "https://github.com/cognitect-labs/test-runner.git"
                       :sha "9e35c979860c75555adaff7600070c60004a0f44"}}
         :main-opts ["-m" "cognitect.test-runner"]
         :exec-fn cognitect.test-runner.api/test}}}

Look how you can reference stuff from git, kind of like in Paket.

Clojure Downsides

So, is Clojure the best programming language ever? Maybe. Can I refactor without fear like I can in F#? No way! It's terrifying 😂.

Conclusion

I feel richly blessed to have been paid to work in F# and Clojure, and for their awesome and welcoming communities! Thanks for reading, Merry Christmas, and may your 2022 be filled with awesome programming languages like F# and Clojure!

This post is part of Sergey Tihon's 8th annual F# Advent Calendar.

Did you find this article valuable?

Support Brett Rowberry by becoming a sponsor. Any amount is appreciated!