We are thrilled to share this article written by Developer Severin Perez exclusively for Signify! This is a great introduction to Functional Programming 'From Objects to Functions', Severin looks at how we can take what we learned from the OOP solutions and convert them to alternate FP solutions in Clojure. Happy learning!
From Objects to Functions
An Introduction to Functional Programming
The Lambda Symbol
One of the best things about programming is that there are many ways to solve the same problem. Ironically, this is also one of the most difficult things about programming. For new programmers, the seemingly endless array of design patterns, best practices, techniques, principles, and all other manner of prescriptive dogma is intimidating at best, and deeply demoralizing at worst. But there is good news hiding among the panoply of architectural choices — as different as they may seem, all such patterns eventually lead back to the same foundational principles. Once you’re familiar with one way to solve a problem, others will follow more easily.
To illustrate how different techniques can be used to solve the same problem, this article will review two different programming paradigms: object-oriented programming (OOP); and, functional programming (FP). Since the vast majority of programmers learn OOP first, we will start by implementing our example programs using OOP patterns in Ruby. From there, we’ll look at how we can take what we learned from the OOP solutions and convert them to alternate FP solutions in Clojure.
A Brief Introduction to Functional Programming
- Functions are Pure: A pure function is one that depends exclusively on its inputs when determining its output. In other words, if you provide a pure function with the same inputs over and over, you will always get the same output. Put another way, pure functions have no side effects — only return values. Compare this to functions in OOP, which may be reliant on some piece of external state. For example, a method on a particular object might access and/or change a separate property held on that object. As a result, the return value from that method could change depending on the current value of the related property. In FP, functions do not depend on external state — they only depend on their arguments.
-
Functions are First-Class: A first-class entity is one that can be passed as an argument to a function. If a language has first-class functions, then they may be passed to other functions as arguments (and by extension, they may receive other functions as arguments). This is absolutely critical in FP because it means that functions may be defined and then passed around a program as a means of interacting with one another.
-
Functions are Higher-Order: A higher-order function is one that either takes another function as an argument or outputs a function as its return value. This concept is an extension of the idea of first-class functions and is used throughout FP. In essence, a higher-order function is one whose behavior may be configured by an input function because it can call that input function in the course of its own execution. As a result, a higher-order function carries out different kinds of work depending on the behavior of the function provided to it as an argument. Common examples of higher-order functions are filtering, mapping, and reducing functions.
If the fundamental units of work in OOP are objects, then the fundamental units of work in FP are functions.
If functions are the core unit of work in FP, then how do they fit together to accomplish a larger task? The answer to this question can be hard to picture in words, which is why we will shortly turn to an example; however, FP programs are generally constructed by linking functions together such that the return value of each function is passed into the next as an argument. In this fashion, larger tasks may be carried out by building a chain of smaller tasks that eventually lead to a final result. A useful way of picturing this is like a math equation (in fact, functional programming has its roots in mathematical logic, specifically, lambda calculus.) Consider the following equation:Here we have a list of linked equations, each of which is reliant on the output of a previous equation. The parentheses in this list help us to determine which “functions” in the equation should be carried out first, their results then acting as arguments in the next “function”. For example, the '8 * 5' equation is executed and the result is then passed in as an argument to the '2 + (previous-result)' part of the equation. The result of that equation is then used as an operand in the '(previous-result) / 7' equation. By working together, these “functions” give us the final result of '6'. Indeed, we can rewrite this mathematical equation as a set of pseudo-code programming functions:
This leads us to two other important FP concepts: referential transparency; and, immutability. A function that is referentially transparent is one that may be replaced with its return value without altering the behavior of a program. Looking at the above set of functions, we could replace the call to 'multiply(8, 5)' with '40' and the final result of the program would be the same. The reason we can do this is that none of these functions rely on external state. In FP, values used in the program should be immutable. If the number '8' was held in a variable, 'my-num', and used as an argument, as in 'multiply(my-num, 5)', then it could be used repeatedly in the same function and always produce the same result because it is immutable. In the above example, this is fairly self-evident; however, this same principle applies to more complex data structures, which should also be treated as immutable in FP.
A Simple Example
Now that we have a good baseline by which to understand an FP program, let’s look at a simple example. In this example, we will write a program to analyze a set of numbers and output the factorial of each number. First, an OOP implementation in Ruby:
class NumberAnalyzer
attr_accessor :numbers
def initialize(numbers)
@numbers = numbers
end
def print_factorials
numbers.each do |num|
puts "#{num}! = #{factorial(num)}"
end
end
private
def factorial(n)
if n == 0
1
else
n * factorial(n - 1)
end
end
end
analyzer = NumberAnalyzer.new([0, 1, 2, 3, 4, 5, 10])
analyzer.print_factorials
# Prints:
# 0! = 1
# 1! = 1
# 2! = 2
# 3! = 6
# 4! = 24
# 5! = 120
In this snippet we have a 'NumberAnalyzer' class that takes an array of numbers as a constructor argument and saves them in a '@numbers' instance variable. The class also has a public 'print_factorials' method that iterates over each number in '@numbers', uses a private 'factorial' method to calculate the current number’s factorial, and then prints out a result string. As you can see, we get the result we expect when we call this function.
One thing to note in this snippet is that the 'NumberAnalyzer' class has a setter method for '@numbers'. Although we do not do so here, we know intuitively that if we were to subsequently use this setter to change the content of '@numbers', then a call to 'print_factorials' would result in a different output. The reason is that 'print_factorials' relies on state held in its particular instance of 'NumberAnalyzer'. If this state changes, so too does the output.
Now let’s take a look at an FP solution written in Clojure:
(ns number-analyzer.core)
(declare print-factorials)
(defn -main [& args]
(print-factorials [0 1 2 3 4 5 10]))
; Prints:
; 0! = 1
; 1! = 1
; 2! = 2
; 3! = 6
; 4! = 24
; 5! = 120
; 10! = 3628800
(defn- factorial [n]
(loop [n n
acc 1]
(if (< n 2)
acc
(recur (dec n) (* acc n)))))
(defn print-factorials [numbers]
(doseq [num numbers]
(println (str num "! = " (factorial num)))))
Don’t worry if the above looks unfamiliar — syntax in Clojure is a bit different from other languages. For now, what’s important is not the Clojure-specific syntax, but rather, the overall flow of the program. Essentially, this snippet operates as follows:
- The 'main' function is automatically executed when the program starts.
- In 'main', we have a call to the 'print-factorials' function, passing in a vector (array) of numbers.
- In 'print-factorials' we use the 'doseq' function to iterate over an input list, iteratively binding each item from 'numbers' to a local 'num' variable.
- On each iteration, we pass 'num' to the 'factorial' function and then pass the result into the 'str' function, which builds an output string, which we then pass to the 'println' function, which prints the string to the screen.
As we’ve seen, OOP and FP programs can be used to solve simple problems in similar ways. Both rely on a series of functions/methods to carry out their work; however, overall program structure and the use (or lack of use) of state differs. Let’s now turn to a slightly more complex example and see if we can observe the same principles at work. In this example, we will build a short program to run an auction. The program has two main tasks: announce the current items available and the overall auction sales; and, run the auction with a list of bids, which will lead to some items being sold and the overall sales total increasing. Here is an OOP version in Ruby:
1 class AuctionHouse
2 attr_accessor :items, :total_sales
3
4 def initialize(items)
5 @items = items
6 @total_sales = 0
7 end
8
9 def run_auction(bid_list)
10 handle_bid_list(bid_list)
11 finalize_sales
12 end
13
14 def announce_status
15 announce_items
16 announce_sales
17 end
18
19 private
20
21 def handle_bid_list(bid_list)
22 bid_list.each { |item, amount| handle_bid(item, amount) }
23 end
24
25 def handle_bid(item, amount)
26 if item_exists?(item) && accept_bid?(item, amount)
27 @items[item] = amount
28 end
29 end
30
31 def item_exists?(item)
32 @items.has_key?(item)
33 end
34
35 def accept_bid?(item, amount)
36 @items[item].nil? || amount > @items[item]
37 end
38
39 def finalize_sales
40 calculate_total_sales
41 remove_sold_items
42 end
43
44 def calculate_total_sales
45 @items.each_pair do |item, amount|
46 @total_sales += amount unless amount.nil?
47 end
48 end
49
50 def remove_sold_items
51 @items = @items.select { |item, amount| @items[item].nil? }
52 end
53
54 def announce_items
55 puts "Available items: "
56 @items.keys.each_with_index { |item, idx| puts "#{idx + 1}: #{item}" }
57 end
58
59 def announce_sales
60 puts "Total sales: #{dollarize_sales}"
61 end
62
63 def dollarize_sales
64 "$" + @total_sales.to_s.reverse.scan(/.{1,3}/).join(",").reverse
65 end
66 end
67
68 paintings = { "Night Watch" => nil, "American Gothic" => nil, "Tower of Babel" => nil,
69 "Friend In Need" => nil, "Potato Eaters" => nil, "Red Balloon" => nil }
70 auction_house = AuctionHouse.new(paintings)
71
72 bids = [["Night Watch", 550000], ["Night Watch", 700000], ["American Gothic", 145000],
73 ["Friend In Need", 180000], ["Potato Eaters", 240000], ["Potato Eaters", 300000],
74 ["Red Balloon", 1500000], ["Red Balloon", 25], ["Red Balloon", 1800000]]
75
76 auction_house.announce_status
77 # Available items:
78 # 1: Night Watch
79 # 2: American Gothic
80 # 3: Tower of Babel
81 # 4: Friend In Need
82 # 5: Potato Eaters
83 # 6: Red Balloon
84 # Total sales: $0
85
86 auction_house.run_auction(bids)
87
88 auction_house.announce_status
89 # Available items:
90 # 1: Tower of Babel
91 # Total sales: $3,125,000
Take a few minutes to look through the above example and follow along with its action. For the sake of brevity, we won’t go through each method; however, at its core this program can be described as follows:
- An 'AuctionHouse' class accepts a list of 'items' when constructing a new instance and saves them, along with an initial 'total_sales' value of '0', to corresponding instance variables.
- Each 'AuctionHouse' instance has two public methods: 'run_auction', which takes a bid list as an argument and then uses it to mutate the '@items' and '@total_sales' variables accordingly; and, 'announce_status', which announces the available items and the total sales.
- When we execute the program, we first announce the status before running the auction, then we run the auction, and then we announce the status again. Of note, after running the auction we have mutated the original item list. Every time an auction runs, the state being carried by the auction house changes.
1 (ns clojure-auction-house.core
2 (:require [clojure.string :as s :only :join]))
3
4 (declare announce-status run-auction)
5
6 (defn -main [& args]
7 (let [auction {:items { "Night Watch" nil "American Gothic" nil "Tower of Babel" nil
8 "Friend In Need" nil "Potato Eaters" nil "Red Balloon" nil }
9 :total-sales 0}
10 bids [["Night Watch" 550000] ["Night Watch" 700000] ["American Gothic" 145000],
11 ["Friend In Need" 180000] ["Potato Eaters" 240000] ["Potato Eaters" 300000],
12 ["Red Balloon" 1500000] ["Red Balloon" 25] ["Red Balloon" 1800000]]]
13 (do
14 (announce-status auction)
15 (announce-status (run-auction auction bids)))))
16
17 ; Available items:
18 ; 1: Night Watch
19 ; 2: American Gothic
20 ; 3: Tower of Babel
21 ; 4: Friend In Need
22 ; 5: Potato Eaters
23 ; 6: Red Balloon
24 ; Total sales: $0
25 ; Available items:
26 ; 1: Tower of Babel
27 ; Total sales: $3,125,000
28
29 (defn item-exists? [items item] (contains? items item))
30
31 (defn accept-bid? [items item amount]
32 (or (= nil (get items item)) (> amount (get items item))))
33
34 (defn handle-bid [items item amount]
35 (if (and (item-exists? items item) (accept-bid? items item amount))
36 (assoc items item amount)
37 items))
38
39 (defn handle-bid-list [items bids]
40 (loop [bid-number 0
41 items items]
42 (if (>= bid-number (count bids))
43 items
44 (let [bid (get bids bid-number)
45 bid-item (first bid)
46 bid-amount (last bid)]
47 (recur
48 (inc bid-number)
49 (handle-bid items bid-item bid-amount))))))
50
51 (defn dollarize [n]
52 (str "$" (s/reverse (s/join "," (re-seq #".{1,3}" (s/reverse (str n)))))))
53
54 (defn build-items-string [items]
55 (loop [counter 0
56 output "Available items:\n"]
57 (if (>= counter (count items))
58 output
59 (recur
60 (inc counter)
61 (str output (inc counter) ": " (get items counter) "\n")))))
62
63 (defn build-sales-string [sales]
64 (str "Total sales: " (dollarize sales)))
65
66 (defn announce-status [auction]
67 (let [items (:items auction)
68 item-names (vec (keys items))
69 sales (:total-sales auction)]
70 (println (str (build-items-string item-names) (build-sales-string sales)))))
71
72 (defn remove-sold-items [items]
73 (into {} (filter (complement (comp some? val)) items)))
74
75 (defn calculate-new-sales [items] (reduce + (filter some? (vals items))))
76
77 (defn finalize-sales [auction]
78 (let [current-items (:items auction)
79 all-sales (calculate-new-sales current-items)]
80 (assoc auction :items (remove-sold-items current-items) :total-sales all-sales)))
81
82 (defn run-auction [auction bids]
83 (let [pre-bid-items (:items auction)
84 post-bid-items (handle-bid-list pre-bid-items bids)]
85 (finalize-sales (assoc auction :items post-bid-items))))
Again, for the sake of brevity, we won’t go through each function, but the general flow can be described as follows.
- The 'main' function binds two initial values to local variables for our 'auction' and 'bids' respectively.
- Once the initial variables are bound, we call 'announce-status' and pass in the 'auction' value, which prints the current auction elements (items and sales) to the terminal.
- We then call 'announce-status' again, but this time, rather than pass in our initial 'auction' value we instead pass in the result of a function call to 'run-auction', with the initial 'auction' and 'bids' values passed to it as arguments.
- The 'run-auction' function calls 'handle-bid-list', using the pre-bid 'items' and the 'bids' list as arguments. After running through a series of helper functions, 'handle-bid-list' returns a new list of items with the various bids applied and binds it to a local 'post-bid-list' variable.
- After the bids have been handled, we then pass a new auction map with the new item list to the 'finalize-sales' function, which removes the sold items and calculates the total sales, returning a new auction map with the updated values.
- Finally, the new auction map gets passed in to our second call to 'announce-status', which prints the new status to the terminal.
If you’re new to FP or to Clojure, the above snippet is likely a bit bewildering, and that’s OK. The most important lessons for now are: the way each function flows into the next one; and, the fact that the initial 'auction' values are never changed during the program, but rather, a totally new auction map is created and eventually printed to the terminal.
TL;DR
Severin Perez is a developer and writer based in Los Angeles, CA. For more of Severin’s writing, you can follow him on Medium, Twitter, or his personal website.