"Deep Dive Into ROM... With Clojure"

July 13, 2015

Recently I read the slides of Deep Dive Into ROM, the latest talk by Piotr Solnica. It's a great talk about how ROM is built and what are the main principles behind its implementation. While reading the Ruby code presented on the slides, code that is very functional in nature, with focus on immutability, I started realizing how close all of this was to a code one could write in Clojure. Let's look at some examples.

On slide 9 we can see the following Ruby code:

class Thing
  attr_reader :value

  def initialize(value)
    @value = value

  def with(value)

thing = Thing.new(1)
other = thing.with(2)

Here Piotr is implementing an immutable object, with handy with method for getting a modified version of the object. The original thing object is never modified.

One of the many great things about Clojure is its immutable, persistent data structures. Let's look at the snippets below:

(conj '(1 2 3) 4) ; => '(4 1 2 3)
(conj [1 2 3] 4) ; => [1 2 3 4]
(conj {:a 1, :b 2} {:a 5}) ; => {:b 2, :a 5}
(conj #{:a :b} :c) ; => #{:c :b :a}

conj is a function which adds a new element to a given collection. Here we add new elements to a list, vector, map and set. The input collection stays unmodified and you get a new one, which shares its structure (previous elements) with the original one.

Thing class example from Ruby could be as simple as this in Clojure:

(def thing {:value 1})
(def other (conj thing {:value 2}))

Let's jump to the slide 11 of the presentation, containing the following code:

class AddTo
  attr_reader :value

  def initialize(value)
    @value = value

  def call(other)
    other + value

add_to = AddTo.new(1)

add_to.call(1) # => 2
add_to.call(2) # => 3

This looks a lot like partial function application in functional languages. In Clojure we can use partial function to achieve similar effect:

(defn add-to [value]
  (partial + value))

(def add-to-one (add-to 1))

(add-to-one 1) ; => 2
(add-to-one 2) ; => 3

Note, that + here isn't any special language keyword (operator). It is the name of the function, that returns a sum of numbers. Thanks to this, we are able to pass it as a function to other higher-order functions like partial.

Now let's look at slides 15 & 16, where we can see a usage of a class which requires some collaborators and configuration:

class StringTransformer
  attr_reader :executor, :operations

  def initialize(executor, operations)
    @executor = executor
    @operations = operations

  def call(input)
    operations.reduce(input) { |a, e|
      executor.call(a, e)

executor = -> str, meth { str.send(meth) }
operations = [:upcase]

upcaser = StringTransformer.new(executor, operations)

upcaser.call('hello world') # => "HELLO WORLD"

This is how a Clojure equivalent could look like:

(defn string-transformer [executor operations]
  (fn [input]
    (reduce executor input operations)))

(defn executor [str f]
  (f str))

(def operations [clojure.string/upper-case])

(def upcaser (string-transformer executor operations))

(upcaser "hello world") ; => "HELLO WORLD"

string-transformer function takes required collaborators (executor) and configuration (operations) and returns a new function that takes one argument and executes reduce operation with it.

Slide 18 shows how proc currying can be used in Ruby:

add = proc { |i, j| i + j }

add_to_one = add.curry.call(1)

add_to_one.call(2) # => 3

While Clojure doesn't have auto-curried functions (it prefers variadic functions) this example can be approximated with partial application:

(defn add [i j] (+ i j))

(def add-to-one (partial add 1))

(add-to-one 2) ; => 3

Or, even shorter:

(def add-to-one (partial + 1))

(add-to-one 2) ; => 3

On slide 21 Piotr shows the same interface (.[]) for executing a default operation (method) on objects of different types:

add = proc { |i, j| i + j }

add[1, 2] # WEIRD?

hash = { a: 1 }
hash[:a] # less weird, right?

arr = [:a, :b]
arr[0] # less weird, right?

add[1, 2] doesn't feel natural in Ruby but it's consistent with hash and array element access (the latter two feel very natural). Interestingly, Clojure's syntax doesn't distinguish between calling a function and accessing an element in a collection:

(defn add [i j]
  (+ i j))

(add 1 2)

(def hash {:a 1})
(hash :a) ; => 1
(:a hash) ; => 1

(def arr [:a :b])
(arr 0) ; => :a

As you can see maps, vectors (also lists and sets) are functions too! They implement IFn interface and thus are callable.

But wait, what about (:a hash) invocation? Keyword (called symbol in Ruby) also implements IFn and can be used to lookup itself in a given map. This is really useful when mapping on a collection of maps because you can pass a keyword as a mapping function to map:

(map :name [{:name "Jane" :age 30} {:name "John" :age 31}]) ; => ("Jane" "John")

Fast-forward to slide 33, we see procs, procs and even more procs:

all_users = [
  { name: 'Jane', email: 'jane@doe.org' },
  { name: 'John', email: 'john@doe.org' }

User = Class.new(OpenStruct)

find_by_name = proc { |arr, name|
  arr.select { |el| el[:name] == name }

map_to_users = proc { |arr, name|
  arr.map { |el| User.new(el) }

users_by_name = proc { |arr, name|
  map_to_users[find_by_name[all_users, name]]

users_by_name[all_users, 'Jane'] # => [#<User name="Jane", email="jane@doe.org">]

So, functions, functions and even more functions in Clojure, right?

(def all-users [{:name "Jane", :email "jane@doe.org" } {:name "John", :email "john@doe.org"}])

(defrecord User [name email])

(defn find-by-name [arr name]
  (filter #(= name (:name %)) arr))

(defn map-to-users [arr]
  (map #(->User (:name %) (:email %)) arr))

(defn users-by-name [arr name]
  (map-to-users (find-by-name arr name)))

(users-by-name all-users "Jane") ; => (User{:email "jane@doe.org", :name "Jane"})

Note that usage of Clojure's record is a bit artificial here but I left it to match the intent of the above Ruby code.

There's one more interesting (and highly functional) piece of code we can look at. Slides 45-47 show this:

class Users < ROM::Relation
  forward :select

  def by_name(name)
    select { |user| user[:name] == name }

class Tasks < ROM::Relation
  forward :select

  def for_users(users)
    user_names = users.map { |user| user[:name] }
    select { |task| user_names.include?(task[:user]) }

user_dataset = [
  { name: 'Jane', email: 'jane@doe.org' },
  { name: 'John', email: 'john@doe.org' }

task_dataset = [
  { user: 'Jane', title: 'Task One' },
  { user: 'John', title: 'Task Two' }

users = Users.new(user_dataset).to_lazy
tasks = Tasks.new(task_dataset).to_lazy

user_tasks = users.by_name >> tasks.for_users

user_tasks.call('Jane').to_a # => [{:user=>"Jane", :title=>"Task One"}]

The above code defines 2 relation classes, instantiates them with actual datasets and combines them into a pipeline. The resulting pipeline can later be called to get the result.

This is no different from function composition in functional programming. Clojure has comp function we can use:

(def all-users [{:name "Jane", :email "jane@doe.org" } {:name "John", :email "john@doe.org"}])
(def all-tasks [{:user "Jane", :title "Task One"} {:user "John", :title "Task Two"}])    

(defn users-by-name [users name]
  (filter #(= name (:name %)) users))

(defn tasks-for-users [tasks users]
  (let [user-names (map :name users)]
    (filter #(some #{(:user %)} user-names) tasks)))

(def my-users-by-name (partial users-by-name all-users))
(def my-tasks-for-users (partial tasks-for-users all-tasks))

(def user-tasks (comp my-tasks-for-users my-users-by-name))

(user-tasks "Jane") ; => ({:title "Task One", :user "Jane"})

Here my-users-by-name and my-tasks-for-users functions are a result of calling partial over users-by-name and tasks-for-users respectively. They're parametrized with all-users and all-tasks, and can be seen as equivalents of users.by_name and tasks.for_users from the Ruby example.

To translate Piotr's users.by_name >> tasks.for_users to Clojure we used comp to compose these 2 functions into a new one, which when called calls the first one with the result of the second one. Note that comp executes the functions in reversed order (unlike Piotr's Ruby version which uses >> operator to nicely visualize the data flow).

To sum up, if you feel that your code looks weird, then maybe it's time to look at other programming languages (not necessarily Clojure). There's a good chance, that you will find one, that suits your current way of thinking, allowing you to express your favourite problems in a simpler, more concise way.

Read more about clojure, ruby.
blog comments powered by Disqus