(log-in "tim@workframe.com" "incorrect password")
; => false
(log-in "tim@workframe.com" "correct password")
; => true
|
You can view these slides online here:
Shortcut: tinyurl.com/ycbkpxxr |
At the end of this talk, you should know how to:
There are a few different ways to think about functions. I’m focusing on two:
Here’s a mathematical function:
f(x) = x2
For every number you put into this function, you get a value out:
f(1) = 1
f(2) = 4
f(3) = 9
|
This is the primary way most Clojure programmers think about functions: they take one or more values in, and they produce a new value. |
Just about everything you can do in Clojure involves defining and calling functions.
You can think of functions as a "black box." You put some stuff into the box, the box does something mysterious, and you get a new thing out of the box.
Consider a web site where you log in with an email address and a password.
(log-in "tim@workframe.com" "incorrect password")
; => false
(log-in "tim@workframe.com" "correct password")
; => true
Behind the scenes, the log-in
function might do a great deal of complex work.
When you call the function you don’t need to know any of those messy details.
All of that complexity is hidden from you, and you only see the output of the function.
|
This notion is called encapsulation and is one of the hallmarks of Clojure programming, and of programming in general. |
Here’s how we would define the square function in Clojure.
;; Define the square function.
;; "defn" is short for "define function"
(defn square [x] (* x x))
;; To call the square function, we just enclose it in parens as usual:
(square 1) ; => 1
(square 2) ; => 4
(square 99) ; => 9801
A function definition has four main parts.
defn
.
(defn square [x] (* x x))
There are a few other ways to define a function, but this is the most common.
(defn square [x] (* x x))
(defn square [x] (* x x))
Parameter lists are always enclosed in square brackets.
(defn square [x] (* x x))
We’ll get back to function definitions a little later.
There’s also a secondary way to think about functions: as a sequence of steps to do in order. The computer-sciencey term for a sequence of steps is a procedure.
Let’s consider a coffee-maker, where you add beans and water, hit a button, and get a pot of coffee.
As a user of the coffee machine, you don’t care how it works.
Most programming languages boil down to ways of telling a computer a series of steps like this.
To call a function, you just enclose the function name in parentheses along with the input values you want to pass to the function. If you do this at the REPL, it will print the response for you.
;; The inc function, short for increment, just adds 1 to its argument
(inc 35) ; => 36
In the above example, inc
is the function name, and
35
is the argument.
Some functions take more than one argument, in which case you just list them after the first argument.
;; The + function adds all of its arguments together.
(+ 1 2) ; => 3
(+ 1 2 3 4 5) ; => 15
;; The str function converts all of its arguments into one big string
(str 123 "hello" 456) ; => "123hello456"
You can pass any type of value into a function as an argument, like maps and vectors:
(def sizes [:small :medium :large])
;; The first function returns the first element in a collection
(first sizes) ; => :small
;; The count function counts the number of items in a collection
(count sizes) ; => 3
;; The sort function sorts a sequence (vector or list)
(sort [-2 33 -1 4]) ; => (-2 -1 4 33)
;; The nth function returns an element of a list at a certain position in it
;; Note that the first element is at position 0, not position 1
(nth [:a :b :c] 1) ; => :b
;; The get function retrieves a value out of a map:
(get {:a 1, :b 2, :c 3} :c) ; => 3
;; Note that keywords are also functions, so you get use this as shorthand:
(:c {:a 1, :b 2, :c 3}) ; => 3
Note that most functions will only operate on certain types of data; if you pass them the wrong type you’ll get an error.
;; Sort needs to sort a list. Giving it a single number is an error:
(sort 75)
; => IllegalArgumentException Don't know how to create ISeq from: java.lang.Long clojure.lang.RT.seqFrom (RT.java:542)
;; The nth function needs to get first a sequence, then a number.
;; The following example is incorrect and throws an error:
(nth 1 [:a :b :c])
; => ClassCastException clojure.lang.PersistentVector cannot be cast to java.lang.Number user/eval1250 (form-init3318102646986532093.clj:1)
You can get information on what arguments a function expects to get using the (doc)
function:
(doc nth)
clojure.core/nth
([coll index] [coll index not-found])
Returns the value at the index. get returns nil if index out of
bounds, nth throws an exception unless not-found is supplied. nth
also works for strings, Java arrays, regex Matchers and Lists, and,
in O(n) time, for sequences.
The built-in docs can be very terse, the online reference http://clojuredocs.org/ tends to have good examples that can help. Here’s nth.
You can nest many function calls in a single Clojure form:
(str "There are " (* 60 24) " minutes in a day.")
; => "There are 1440 minutes in a day."
The way this works is that the inner bit, (* 60 60)
, is evaluated to get its
result, 1440. Then Clojure proceeds to call the str
function as:
(str "There are " 1440 " minutes in a day.")
You can nest function calls arbitrarily. Generally, they will be evaluated innermost first.
;; Fruits is a vector where each element is a map
(def fruits [{:type :apple :price 2.0}
{:type :pear :price 3.0}
{:type :orange :price 1.0}])
(str "Your total cost is " (* 5 (get (nth fruits 1) :price)))
; => "Your total cost is 15.0"
;; If we break down the above step by step, it looks like this:
(nth fruits 1) ; => {:type :pear price 3.0}
(str "Your total cost is " (* 5 (get {:type :pear price 3.0} :price)))
(get {:type :pear price 3.0} :price) ; => 3.0
(str "Your total cost is " (* 5 3.0))
(* 5 3.0) ; => 15.0
(str "Your total cost is " 15.0)
Here’s how we might define our coffee-maker from earlier:
(defn grind-coffee [beans] ...)
(defn add-grinds-to-filter [grinds] ...)
(defn boil-water [cold-water] ...)
(defn pour-water-through-filter [hot-water filter-with-grinds] ...)
(defn make-coffee [beans cold-water]
(pour-water-through-filter
(boil-water cold-water)
(add-grinds-to-filter (grind-coffee beans))))
Note how we’re breaking a large task up into a series of smaller tasks.
Earlier we defined the square
function, with a single argument x
.
(defn square [x] (* x x))
We can also define a function that takes in no arguments:
(defn pi [] 3.14)
;; You call it by enclosing it in parens as usual:
(pi) ; => 3.14
Or two arguments:
(defn average [first-number second-number]
(/ (+ first-number second-number) 2.0))
(average 3 6) ; => 4.5
You can provide as many arguments as you like to a function, and you can name them whatever you want.
(defn make-coffee [beans water electricity] ...)
(defn log-in [email-address password] ...)
Function names are symbols and they have the same rules for what you can name them.
?
and *
and >
.
(defn hello-world [] "hello!")
There are a few conventions for naming functions.
(make-coffee)
(log-in)
(take-out-trash)
A function which returns either true
or false
is called a predicate.
(string? "hello, world") ; => true
(even? 45) ; => false
(contains? {:a 1, :b 2, :c 3} :a) ; => true
The common convention for predicates is to end them with a ?
character.
A function body (the part after the parameter list) can contain any number of other forms.
Only the last form in a function is used as the return value.
Everything else in the function body is evaluated, but their results are not used.
(defn weird-average [a b]
(+ 2 2) ; This will be evaluated as 4, but discarded
:hello ; This is also discarded
(/ (+ a b) 2.0)) ; This is the last form, so it is the return value
Why would you want to evaluate something you don’t keep around? Side effects.
A side effect is anything that happens inside of a function that changes something outside of the function.
The most common example is I/O, or input/output from the program.
(defn chatty-average [a b]
(println "a is" a "and b is" b)
(/ (+ a b) 2.0))
When you run this, you’ll see some output from it:
user=> (chatty-average 1 2)
a is 1 and b is 2
1.5
user=>
Note the line a is 1 and b is 2
above. The actual return value is 1.5
.
|
As a functional programming language, Clojure discourages the use of side effects. |
Some functions take other functions as input.
map
takes a function and applies it to every element in a list.
(inc 32) ; => 33
(inc 0) ; => 1
(inc 77) ; => 78
(map inc [32 0 77]) ; => (33 1 78)
(map square [1 2 3 4]) ; => (1 4 9 16)
You pass a function to another one by just writing its name, without parentheses.
;; This is incorrect!
;; Clojure tries to evaluate (inc), but inc requires one argument
(map (inc) [1 2 3])
; CompilerException clojure.lang.ArityException: Wrong number of args (0) passed to: core/inc--inliner--6556
filter
takes a predicate and uses it to filter elements of a sequence
[a b & rest]
arguments
(let)
statement