You will learn
- Name values globally with
defand locally withlet - Define functions with
defn, parameters, and a docstring - Write anonymous functions with
fnand#(...)and pass them toupdate - Give one function several arities, or any number of arguments with
& - Call a function with the items of a collection using
apply
So far you have typed values and called built-in functions. Now you give things names and write your own functions. A function is the main unit of reuse in Phel: small functions that each do one thing, combined into bigger ones. Every module after this one builds on what you learn here.
Naming values with def
def binds a global name to a value. After that, you can use the name anywhere the value would go.
(def pi 3.14159)
(* pi 2) ; => 6.28318Exercise 1 Predict easy
What does the last line return?
(def tax-rate 0.2)
(* 100 tax-rate)
Solution
(def tax-rate 0.2)
(* 100 tax-rate) ; => 20.0
Phel replaces tax-rate with its value, 0.2. An integer times a float gives a float, so the result is 20.0, not 20.
Exercise 2 Write easy
Bind the name greeting to "Hello, Phel!". Then use greeting and str to build this string:
; => "Hello, Phel! Nice to meet you."
Solution
(def greeting "Hello, Phel!")
(str greeting " Nice to meet you.") ; => "Hello, Phel! Nice to meet you."
def creates a global binding: a name that points at a value. In PHP you would write $greeting = "Hello, Phel!";. Use def for values that the whole program shares.
Learn more: Global and Local Bindings
Exercise 3 Predict easy
Now that you can name a value, check that "changing" a collection leaves the original alone. What do the last two lines return?
(def langs ["php" "phel"])
(conj langs "clojure")
langs
Solution
(def langs ["php" "phel"])
(conj langs "clojure") ; => ["php" "phel" "clojure"]
langs ; => ["php" "phel"]
conj returns a new vector and leaves langs as it was. To keep the new vector, give it a name: (def more-langs (conj langs "clojure")). In PHP, $langs[] = "clojure"; changes the array in place. In Phel nothing changes behind your back.
Learn more: Data Structures
Local names with let
let takes a vector of name and value pairs, then a body. The names exist only inside the body. A later name can use an earlier one.
(let [price 50
qty 3]
(* price qty)) ; => 150Exercise 4 Fill in easy
Fill in the blanks so the expression returns the area of a rectangle that is 5 wide and 3 high.
(let [width 5
height ___]
(___ width height))
; => 15
Solution
(let [width 5
height 3]
(* width height))
; => 15
Stack as many pairs as you need in the let vector. Local names keep the global namespace clean.
Learn more: Global and Local Bindings
Exercise 5 Predict medium
What does the let return, and what does x return on the last line?
(def x 1)
(let [x 10
y (* x 2)]
(+ x y))
x
Hint
Inside the let, the local x hides the global one. Which x does y see?
Solution
(def x 1)
(let [x 10
y (* x 2)]
(+ x y)) ; => 30
x ; => 1
Inside the let, x is 10, so y is 20 and the sum is 30. The local x only hides the global one inside the body. Outside, x is still 1.
Defining functions with defn
defn defines a named function: a name, a vector of parameters, and a body. The function returns the value of its last expression. There is no return keyword. A docstring after the name describes what the function does.
(defn square
"Multiplies n by itself."
[n]
(* n n))
(square 7) ; => 49Exercise 6 Write easy
Define a function twice that multiplies a number by 2.
(twice 5) ; => 10
(twice -3) ; => -6
Solution
(defn twice [n]
(* n 2))
(twice 5) ; => 10
(twice -3) ; => -6
[n] is the parameter list. The body (* n 2) is the last expression, so its value is what the function returns.
Learn more: Functions and Recursion
Exercise 7 Write easy
Define full-name with two parameters and a docstring. It joins a first and a last name with a space.
(full-name "Ada" "Lovelace") ; => "Ada Lovelace"
Solution
(defn full-name
"Joins a first and a last name with a space."
[first-name last-name]
(str first-name " " last-name))
(full-name "Ada" "Lovelace") ; => "Ada Lovelace"
The docstring goes between the name and the parameter list. In the REPL, (doc full-name) prints it back.
Learn more: Functions and Recursion
Exercise 8 Fix medium
This function does not compile. Read the error, then fix the function so that (area 5 3) returns 15.
(defn area [width]
(* width height))
(area 5 3)
; ERROR: Cannot resolve symbol 'height'
Hint
A function only sees its own parameters, its local names, and global names. Where should height come from?
Solution
(defn area [width height]
(* width height))
(area 5 3) ; => 15
height was never a parameter, so Phel cannot find it. Every value the function needs from the caller goes in the parameter vector, in the order the caller passes them.
Exercise 9 Write medium
Define shipping-cost. A parcel costs a base fee of 5, plus 2 for each kilogram. Use let inside the function to name the base fee and the price per kilogram.
(shipping-cost 0) ; => 5
(shipping-cost 3) ; => 11
Hint
The body of defn can be a let. The value of the let becomes the return value of the function.
Solution
(defn shipping-cost [kg]
(let [base-fee 5
per-kg 2]
(+ base-fee (* per-kg kg))))
(shipping-cost 0) ; => 5
(shipping-cost 3) ; => 11
Naming base-fee and per-kg tells the reader what the numbers mean. Without the names, (+ 5 (* 2 kg)) works but hides the rules.
Anonymous functions
fn creates a function without a name. #(...) is a shorter form, where % is the first argument (%1, %2 when there are several). Use them when you need a small function once, for example as the last argument of update.
(update {:name "Ada" :visits 2} :visits (fn [n] (+ n 10)))
; => {:name "Ada" :visits 12}Exercise 10 Predict easy
What do these two expressions return?
((fn [x] (+ x 10)) 5)
(#(* %1 %2) 3 4)
Solution
((fn [x] (+ x 10)) 5) ; => 15
(#(* %1 %2) 3 4) ; => 12
The first position of a list is the function to call. Here that function is written in place. #(* %1 %2) is the same as (fn [a b] (* a b)).
Learn more: Functions and Recursion
Exercise 11 Refactor medium
This code reads the total, changes it, and writes it back. Rewrite it with update and a #(...) function, so total is named only once.
(def cart {:items 3 :total 50})
(assoc cart :total (* (get cart :total) 0.9))
; => {:items 3 :total 45.0}
Hint
update passes the old value to the function you give it and stores what the function returns.
Solution
(def cart {:items 3 :total 50})
(update cart :total #(* % 0.9))
; => {:items 3 :total 45.0}
update does the read and the write for you. You only describe the change: "multiply by 0.9". The original cart does not change.
Flexible arguments
A function can have one body per number of arguments. This is called multi-arity. Wrap each parameter vector and its body in parentheses. A short arity often calls a longer one with a default.
(defn price
([amount] (price amount 0))
([amount discount] (- amount discount)))
(price 100) ; => 100
(price 100 15) ; => 85Exercise 12 Write medium
Define greet. With one argument it says "Hello". With two, the first argument is the greeting to use.
(greet "Ada") ; => "Hello, Ada!"
(greet "Welcome" "Ada") ; => "Welcome, Ada!"
Hint
Write the two-argument version first. Then let the one-argument version call it with "Hello".
Solution
(defn greet
([name] (greet "Hello" name))
([greeting name] (str greeting ", " name "!")))
(greet "Ada") ; => "Hello, Ada!"
(greet "Welcome" "Ada") ; => "Welcome, Ada!"
The one-argument arity fills in the default and hands over to the two-argument arity. The formatting rule lives in one place. In PHP you would write a default parameter value, function greet($name, $greeting = "Hello").
Learn more: Functions and Recursion
Exercise 13 Predict medium
apply calls a function with the items of a collection as its arguments. What do these return?
(apply + [1 2 3])
(apply max 4 [1 9])
(apply str ["a" "b" "c"])
Hint
Rewrite each call without apply: take the items out of the vector and put them in the argument list.
Solution
(apply + [1 2 3]) ; => 6
(apply max 4 [1 9]) ; => 9
(apply str ["a" "b" "c"]) ; => "abc"
(apply + [1 2 3]) is the same as (+ 1 2 3). Arguments before the collection come first, so (apply max 4 [1 9]) is (max 4 1 9).
Learn more: Functions and Recursion
Exercise 14 Write hard
Define log-line. It takes a level, then any number of text parts, and joins them after the level in brackets.
(log-line "INFO" "user " "Ada " "logged in") ; => "[INFO] user Ada logged in"
(log-line "WARN" "disk almost full") ; => "[WARN] disk almost full"
(log-line "DEBUG") ; => "[DEBUG] "
Hint
In a parameter vector, & parts collects every remaining argument into one collection. Then you need a way to pass that collection to str.
Solution
(defn log-line [level & parts]
(str "[" level "] " (apply str parts)))
(log-line "INFO" "user " "Ada " "logged in") ; => "[INFO] user Ada logged in"
(log-line "WARN" "disk almost full") ; => "[WARN] disk almost full"
(log-line "DEBUG") ; => "[DEBUG] "
level takes the first argument and parts collects the rest, which can be none. A common mistake is (str "[" level "] " parts): that puts the printed collection in the string, not its items.
Learn more: Functions and Recursion
What you can do now
- You can name a value with
defand keep helper names local withlet - You can define, document, and call your own functions
- You can write a small anonymous function and hand it to
update - You can write functions that accept optional or extra arguments
- You can spread a collection into a function call with
apply