Skip to content

Latest commit

 

History

History
503 lines (358 loc) · 15.2 KB

index.adoc

File metadata and controls

503 lines (358 loc) · 15.2 KB

ClojureBridge NYC 2017: Intro to Clojure Functions

Agenda

  • 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:

Goals

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

What is a function?

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.

The mathy way to think about functions

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.

More on that black box idea

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.

Sneak peek: defining your own functions

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

Elements of a function definition

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.

The procedural way to think about functions

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.

Making 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.

Calling functions

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"

More about function arguments

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

Aside: errors

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.

Nesting function calls

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)

Back to the grind

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.

Back to defining functions: parameter lists

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

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.

Common function conventions
  • Functions should be all lower-case

  • Functions with more than one word should be separated by dashes.

    (make-coffee) (log-in) (take-out-trash)

Predicates

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.

Function bodies and side effects

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.

Bonus: higher-order functions

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

Bonus: advanced topics

  • Functions that take [a b & rest] arguments

  • Threading macros

  • The (let) statement

  • Destructuring

Q & A