Skip to main content

Names and Functions

You will learn

  • Name values globally with def and locally with let
  • Define functions with defn, parameters, and a docstring
  • Write anonymous functions with fn and #(...) and pass them to update
  • Give one function several arities, or any number of arguments with &
  • Call a function with the items of a collection using apply
Progress 0/14

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

Exercise 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)) ; => 150

Exercise 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) ; => 49

Exercise 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) ; => 85

Exercise 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 def and keep helper names local with let
  • 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