You will learn
- Build vectors, maps, sets, and lists with literal syntax
- Read values with
get,first,rest, and keywords as functions - Add and remove with
conj,assoc, anddissoc, knowing the old value stays the same - Read and change nested data with
get-in,assoc-in,update, andupdate-in
Read first
Real programs are mostly data: a user, a cart, a list of orders. In Phel you hold that data in four collections: vectors, maps, sets, and lists. They are immutable: every "change" gives you a new collection, and the old one stays as it was. That one rule removes a whole family of bugs, and this module shows you how to work with it.
Vectors
A vector is an ordered list of values in square brackets. It is the Phel version of a PHP list like [1, 2, 3], without the commas. Indexes start at 0.
[10 20 30] ; a vector of three numbers
(get [10 20 30] 0) ; => 10
(conj [10 20 30] 40) ; => [10 20 30 40]Exercise 1 Predict easy
Predict each result.
(count [10 20 30])
(first [10 20 30])
(rest [10 20 30])
(get [10 20 30] 1)
Solution
(count [10 20 30]) ; => 3
(first [10 20 30]) ; => 10
(rest [10 20 30]) ; => [20 30]
(get [10 20 30] 1) ; => 20
first gives the first item, rest gives everything after it. get with index 1 gives the second item, because indexes start at 0.
Exercise 2 Fill in easy
Replace ___ so each form returns the value in its comment.
(conj [:a :b] ___) ; => [:a :b :c]
(get [:a :b :c] ___) ; => :c
(get [:a :b :c] 10 ___) ; => :none
Hint
get takes an optional third argument: the value to return when nothing is found.
Solution
(conj [:a :b] :c) ; => [:a :b :c]
(get [:a :b :c] 2) ; => :c
(get [:a :b :c] 10 :none) ; => :none
A missing index is not an error: get returns nil, or your default if you give one.
Maps
A map holds key-value pairs in curly braces. Keys are usually keywords. In PHP this is an associative array like ['name' => 'Ada'].
{:name "Ada" :age 36}
(get {:name "Ada" :age 36} :name) ; => "Ada"Exercise 3 Predict easy
Predict each result.
(get {:name "Ada" :age 36} :age)
(get {:name "Ada" :age 36} :email)
(get {:name "Ada" :age 36} :email "unknown")
(count {:name "Ada" :age 36})
Solution
(get {:name "Ada" :age 36} :age) ; => 36
(get {:name "Ada" :age 36} :email) ; => nil
(get {:name "Ada" :age 36} :email "unknown") ; => "unknown"
(count {:name "Ada" :age 36}) ; => 2
A missing key gives nil, not an error. PHP would warn about an undefined array key. count counts key-value pairs.
Exercise 4 Fill in easy
assoc adds or replaces a key. dissoc removes one. Replace ___ so each form returns the value in its comment.
(assoc {:name "Ada"} ___ ___) ; => {:name "Ada", :age 36}
(assoc {:name "Ada"} :name ___) ; => {:name "Grace"}
(dissoc {:name "Ada" :age 36} ___) ; => {:name "Ada"}
Solution
(assoc {:name "Ada"} :age 36) ; => {:name "Ada", :age 36}
(assoc {:name "Ada"} :name "Grace") ; => {:name "Grace"}
(dissoc {:name "Ada" :age 36} :age) ; => {:name "Ada"}
When the key already exists, assoc replaces its value. The REPL prints commas between pairs to help you read; in your code they are optional.
Exercise 5 Predict easy
Predict each result.
(keys {:name "Ada" :age 36})
(vals {:name "Ada" :age 36})
(contains? {:name "Ada" :age 36} :age)
(contains? {:name "Ada" :age 36} :email)
Solution
(keys {:name "Ada" :age 36}) ; => [:name :age]
(vals {:name "Ada" :age 36}) ; => ["Ada" 36]
(contains? {:name "Ada" :age 36} :age) ; => true
(contains? {:name "Ada" :age 36} :email) ; => false
keys and vals split a map into its two sides. contains? asks about the key, not the value. It is PHP's array_key_exists.
Sets and lists
A set holds unique values, written #{...}. Adding a value that is already there changes nothing. A list is written '(1 2 3). The quote ' tells Phel "this is data, do not call 1". You will see lists more in code than in data.
#{:red :green} ; a set
'(1 2 3) ; a list
(contains? #{:red :green} :red) ; => trueExercise 6 Predict medium
conj means "add in the natural way for this collection". Predict each result.
(conj [1 2] 3)
(conj '(1 2) 3)
(conj #{1 2} 2)
(conj #{1 2} 3)
Hint
A vector grows at the end. A list grows at the front. A set never holds the same value twice.
Solution
(conj [1 2] 3) ; => [1 2 3]
(conj '(1 2) 3) ; => (3 1 2)
(conj #{1 2} 2) ; => #{1 2}
(conj #{1 2} 3) ; => #{1 2 3}
Each collection adds where it is fastest. If you need order and want to add at the end, use a vector.
Learn more: Adding with conj
Nothing changes in place
In PHP, $cart[] = 'pear' changes $cart. In Phel, (conj cart "pear") returns a new vector, and cart stays the same. Nothing can change a value behind your back. So when you want several changes, you pass the result of one change into the next, from the inside out.
(assoc (assoc {} :a 1) :b 2) ; => {:a 1, :b 2}Exercise 7 Write medium
Start from this map:
{:name "Ada" :role :guest}
Write one form that sets :role to :admin and then removes :name. It should return:
{:role :admin}
Hint
The first change goes on the inside. Wrap the second change around it.
Solution
(dissoc (assoc {:name "Ada" :role :guest} :role :admin) :name)
; => {:role :admin}
assoc builds a new map, then dissoc builds another one from it. The first map is never changed. In the Sequences module you will learn ->, which lets you write this top to bottom.
Keywords as functions
A keyword can look itself up in a map: (:name user) is the same as (get user :name). This is the most common way to read a field in Phel. Maps and sets can be called too.
(:name {:name "Ada"}) ; => "Ada"Exercise 8 Predict medium
Predict each result.
(:age {:name "Ada" :age 36})
(:email {:name "Ada"})
(:role {:name "Ada"} :guest)
({:name "Ada"} :name)
(#{:red :green} :blue)
Hint
Every one of these works like get, including the default value.
Solution
(:age {:name "Ada" :age 36}) ; => 36
(:email {:name "Ada"}) ; => nil
(:role {:name "Ada"} :guest) ; => :guest
({:name "Ada"} :name) ; => "Ada"
(#{:red :green} :blue) ; => nil
Calling a set asks "is this value in you?": you get the value back, or nil.
Learn more: Data structures as functions
Nested data
Real data nests: a map holds a vector, which holds maps. The -in functions take a path, a vector of keys and indexes, and walk it for you. update changes one value with a function, like inc (add one).
(get-in {:user {:city "Berlin"}} [:user :city]) ; => "Berlin"
(assoc-in {:user {:city "Berlin"}} [:user :city] "Rome") ; => {:user {:city "Rome"}}
(update {:visits 1} :visits inc) ; => {:visits 2}Exercise 9 Write medium
Write a form that finds :treasure in this dungeon.
{:description "dark cave"
:rooms [{:contents :monster}
nil
{:contents [:trinket :treasure]}]}
Hint
Write the path one step at a time: which key, then which room index, then which key, then which index.
Solution
(get-in {:description "dark cave"
:rooms [{:contents :monster}
nil
{:contents [:trinket :treasure]}]}
[:rooms 2 :contents 1])
; => :treasure
Keys and indexes mix freely in one path. If any step is missing, get-in returns nil instead of failing.
Exercise 10 Predict medium
Predict each result.
(update {:visits 1} :visits inc)
(update {:visits 1} :visits dec)
(update [1 2 3] 0 inc)
(get-in {:user {:city "Berlin"}} [:user :phone :number])
Hint
For a vector, the "key" is the index.
Solution
(update {:visits 1} :visits inc) ; => {:visits 2}
(update {:visits 1} :visits dec) ; => {:visits 0}
(update [1 2 3] 0 inc) ; => [2 2 3]
(get-in {:user {:city "Berlin"}} [:user :phone :number]) ; => nil
update reads the old value, calls the function on it, and puts the result back. You write "what to do", not "read, change, write".
Exercise 11 Write hard
Add one pear to this cart. Write one form that returns the cart with the pear's :qty raised from 1 to 2.
{:items [{:name "apple" :qty 2}
{:name "pear" :qty 1}]}
Expected result:
{:items [{:name "apple" :qty 2}
{:name "pear" :qty 2}]}
Hint
update-in is update with a path. What is the path to the pear's quantity?
Solution
(update-in {:items [{:name "apple" :qty 2}
{:name "pear" :qty 1}]}
[:items 1 :qty]
inc)
; => {:items [{:name "apple", :qty 2} {:name "pear", :qty 2}]}
The path goes into :items, then index 1, then :qty, and inc does the change. A common mistake is (assoc-in ... [:items 1 :qty] 2): it works here, but it hardcodes the new number instead of adding one.
Exercise 12 Fix hard
This form should switch the theme to "light". Run it. It does not fail, but the result is wrong. Fix it.
(assoc {:user {:name "Ada" :settings {:theme "dark"}}} :theme "light")
Hint
Compare the result with the input. Where did the new :theme go?
Solution
(assoc-in {:user {:name "Ada" :settings {:theme "dark"}}}
[:user :settings :theme]
"light")
; => {:user {:name "Ada", :settings {:theme "light"}}}
assoc only works on the top level, so the broken form added a new :theme key next to :user and left the real one unchanged. For nested data, use assoc-in with the full path.
What you can do now
- You can pick the right collection: vector for order, map for named fields, set for unique values
- You can read any value from a collection, even a missing one, without an error
- You know that
conj,assoc, anddissocreturn a new collection and never change the old one - You can read and change nested data with a path of keys