-
What is a function and what are they useful for?
-
How do I call a function?
-
How do I define my own functions?
-
Some advanced topics, time permitting
-
Q & A
Note
|
You can view these slides online here:
Shortcut: tinyurl.com/ycbkpxxr |
At the end of this talk, you should know how to:
-
Understand what functions are and what they’re good for
-
Define your own function
-
Call a function from the standard library or one you’ve defined
There are a few different ways to think about functions. I’m focusing on two:
-
Mathy way: a function is a piece of code that takes a value as input and returns a new value as output.
-
Procedural way: a function is a way of executing a bunch of different steps in order.
Here’s a mathematical function:
f(x) = x^2^
For every number you put into this function, you get a value out:
-
f(1) = 1
-
f(2) = 4
-
f(3) = 9
Note
|
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.
-
Input: email address and password
-
Output: true or false
(log-in "[email protected]" "incorrect password")
; => false
(log-in "[email protected]" "correct password")
; => true
Behind the scenes, the log-in
function might do a great deal of complex work.
-
Connect to a database
-
Contact Facebook to validate the user
-
Return a random result every time you call it
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.
Note
|
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.
-
Most function definitions will start with
defn
.(defn square [x] (* x x))
There are a few other ways to define a function, but this is the most common.
-
The function name tells Clojure what your function is called.
(defn square [x] (* x x))
-
The parameter list tells Clojure what type of input, or arguments, your function takes.
(defn square [x] (* x x))
Parameter lists are always enclosed in square brackets.
-
The function body tells Clojure how to generate the output, or result, of the function.
(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.
-
Grind the coffee beans
-
Put the ground coffee into the filter
-
Boil the water
-
Pour the water through the coffee grounds
As a user of the coffee machine, you don’t care how it works.
-
You give it coffee beans and water, and it gives you coffee.
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.
-
Basically, they can’t start with a number, and they can consist of numbers, letters, dashes, and a bunch of special characters such as
?
and*
and>
.
(defn hello-world [] "hello!")
There are a few conventions for naming functions.
-
Functions should be all lower-case
-
Functions with more than one word should be separated by dashes.
(make-coffee)
(log-in)
(take-out-trash)
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
.
Note
|
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
-
Functions that take
[a b & rest]
arguments -
Threading macros
-
The
(let)
statement -
Destructuring