Clojure Features: Or why you should stop worrying about features and just use Clojure
Introduction
Back in November 2017, a blog post popped up which discussed a series of language features that one would love to see present in any programming language.
In response, I created a github gist which showed off many equivalent features in clojurescript.
Instead of recapping on the gist, I feel like a lot was left absent. There are a ton of features out there which were never mentioned. Every flavour or trend you can think of in recent developments, you can find a taste of it in clojure and clojurescript.
I could talk about clojure's strengths in replicating language features through homoiconicity, but i'd rather just step through what clojure and clojurescript has right now, and how you can learn more about each one.
Golang - Channels
The Go Programming language has this concept of Channels, quoting GoByExample.com:
Channels are the pipes that connect concurrent goroutines. You can send values into channels from one goroutine and receive those values into another goroutine.
An example within Go:
package main
import "fmt"
func main() {
messages := make(chan string)
go func() { messages <- "ping" }()
msg := <-messages
fmt.Println(msg)
}
core.async
Some clojure devs decided they wanted something similar to goroutines and channels, so they developed core.async.
Here's the Go example written in core.async:
(require '[clojure.core.async :refer [go chan >! <!!]])
(def messages (chan))
(go (>! messages "ping"))
(println (<!! messages))
core.async can also be used in clojurescript, and leaves you dealing with less asynchronous cruft in frontend development.
For a better breakdown on what core.async has to offer, I highly recommend reading Chapter 11: Mastering Concurrent Processes with core.async in the book Clojure For the Brave and True.
Rust - Pattern Matching
Rust makes great use of built-in pattern matching using the match
keyword. Quoting the rust book:
match allows us to compare a value against a series of patterns and then execute code based on which pattern matches. Patterns can be made up of literal values, variable names, wildcards, and many other things.
An example within Rust:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
core.match
core.match was developed to resemble pattern matching within erlang, but since Rust is becoming more popular, it fit as a better comparison.
Here's an example of core.match, which is similar to the Rust example:
(require '[clojure.core.match :refer [match]])
(defn value-in-cents [coin]
(match [coin]
:penny 1
:nickel 5
:dime 10
:querter 25))
Here is a much broader overview of what can be done with core.match.
Javascript - JSX
One upcoming feature within javascript is Javascript Syntax Extensions. This feature was introduced to represent HTML elements within rendered ReactJS Components:
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
ReactDOM.render(
element,
document.getElementById('root')
);
This requires the use of the tool babel to compile the next generation of javascript into something that is understood by most web browsers.
Sablono
Sablono is similar to JSX, in that it converts a data structure into React DOM elements. Its representation is based on the markup language hiccup.
Several clojurescript libraries make use of Sablono when wrapping ReactJS. A popular one is rum, which is an excellent library taking advantage of ReactJS, while leveraging a lot of things that make clojurescript great.
Here is the JSX example represented using rum:
(require '[rum.core :as rum])
(defn format-name [{:keys [first-name last-name]}]
(str first-name " " last-name))
(def user {:first-name "Harper"
:last-name "Perez"})
(rum/defc element []
[:h1 (str "Hello, " (format-name user))])
(rum/mount (element) (.getElementById js/document "root"))
Another popular clojurescript library which employs its own take on the hiccup syntax, and which also wraps ReactJS is reagent.
Prolog - Logic Programming
It is unfair to say that Prolog features logic programming, since Prolog is a logic programming language. The Prolog language is used heavily in artificial intelligence and computational linguistics.
Here is an example of a logic program written in prolog:
mother_child(trude, sally).
father_child(tom, sally).
father_child(tom, erica).
father_child(mike, tom).
sibling(X, Y) :- parent_child(Z, X), parent_child(Z, Y).
parent_child(X, Y) :- father_child(X, Y).
parent_child(X, Y) :- mother_child(X, Y).
And here is a relevant logic query on the logic program:
?- sibling(sally, erica).
Yes
core.logic
Logic engines exist in many languages, but clojure has the advantage of incorporating the engine in a way that is truly beautiful. The core.logic implementation is based off of miniKanren. The original implementation of miniKanren is written in Scheme, and is presented in the book The Reasoned Schemer, a recommended read.
Here is an example of the prolog example in core.logic:
(require '[clojure.core.logic :refer :all])
(require '[clojure.core.logic.pldb :as pldb]) ;; Immutable Fact Database
;; DB Relations
(pldb/db-rel mother-child mother child)
(pldb/db-rel father-child father child)
;; Creating the DB with the facts
(def facts
(-> (pldb/db)
(pldb/db-fact mother-child :trude :sally)
(pldb/db-fact father-child :tom :sally)
(pldb/db-fact father-child :tom :erica)
(pldb/db-fact father-child :mike :tom)))
(defn parent-child [X Y]
(conde
[(mother-child X Y)]
[(father-child X Y)]))
(defn sibling [X Y]
(fresh [Z]
(parent-child Z X)
(parent-child Z Y)))
(run-db 1 facts [_]
(sibling :sally :erica)) ;; (_0), which is 'true'
There are a few articles which talk about the uses for logic programming, but the recommended approach to learn about its ins and outs is to dive into the Learn Prolog Now! tutorials, or to read through and follow the examples in The Reasoned Schemer.
(Programming Lang) - (Lang Feature)
So far, i've covered ~4 features, with examples showing similar results in clojure(script). If there's an interesting feature that you think hasn't been recreated in clojure, or you think is difficult to recreate in clojure, I would love to hear about it! You can find me on the clojurians slack as @benzap.