28-05-2014
Michiel Borkent
Korte uitleg van deze begrippen aan de hand van Clojure.
Eerst: hoe kun je de voorbeelden uit deze sheets zelf uitproberen?
REPL: Read Eval Print Loop
1 user=> <- prompt
REPL: Read Eval Print Loop
1 user=> (inc 1) <- expressie
2 2 <- resultaat
REPL: Read Eval Print Loop
1 user=> (println "hello world") <- expressie
2 hello world <- side effect
3 nil <- resultaat
project.clj
, vergelijkbaar met pom.xml
van Maven, maar
dan Clojure-notatie ipv XMLVersie is dus per project makkelijk in te stellen.
1 (defproject gastcollege "0.1.0-SNAPSHOT"
2 :description "FIXME: write description"
3 :url "http://example.com/FIXME"
4 :license {:name "Eclipse Public License"
5 :url "http://www.eclipse.org/legal/epl-v10.html"}
6 :dependencies [[org.clojure/clojure "1.5.1"]])
Run as Clojure application
1 (inc 1)
in plaats van:
1 inc(1)
Dit noem je ook wel prefix-notatie.
1 (if (< (rand-int 10) 5)
2 "Getal kleiner dan 5"
3 "Getal groter dan of gelijk aan 5")
1 (let [x (+ 1 2 3)
2 y (+ 4 5 6)
3 z (+ x y)]
4 z) ;;=> 21
Place oriented programming
Value oriented programming
Conceptueel zoals wiskundige functie:
De meeste functies in Clojure zijn puur.
1 (inc 10) ;;=> 11
2 (inc 11) ;;=> 12
3 (count "foo") ;;=> 3
4 (count "dude") ;;=> 4
5 (str "foo" "bar") ;;=> "foobar"
Niet-pure functies zijn echter gewoon mogelijk (i.t.t. Haskell)
1 (rand-int 10) ;;=> 7
2 (rand-int 10) ;;=> 0
3 (time (inc 1)) ;;=>
4 "Elapsed time: 0.022 msecs" <- side effect
5 2 <- resultaat
6 user=> (time (inc 1))
7 "Elapsed time: 0.024 msecs" <- side effect
8 2 <- resultaat
rand-int
en time
niet puur?time
is een speciaal soort functie, nl. een macro 1 (def game-state (atom {:score 0}))
2
3 (defn increase-score [old-state points]
4 (update-in old-state [:score] + points))
5
6 ;; test:
7 (increase-score {:score 40} 20) ;;=> {:score 60}
8
9 (defn score! []
10 (swap! game-state increase-score 20))
11
12 @game-state ;;=> {:score 0}
13 (score!)
14 @game-state ;;=> {:score 20}
15 (score!)
16 @game-state ;;=> {:score 40}
1 (defn reverse-str [s]
2 (apply str
3 (reverse s)))
4
5 (reverse-str "foo") ;;=> "oof"
Sequentieel
1 (map reverse-str ["foo" "bar" "baz"])
2 ;;=> ("oof" "rab" "zab")
Parallel (1 letter verschil)
1 (pmap reverse-str ["foo" "bar" "baz"])
2 ;;=> ("oof" "rab" "zab")
1 (map inc [1 2 3]) ;;=> (2 3 4)
inc
1 (odd? 1) ;;=> true
2 (odd? 2) ;;=> false
3 (range 10) ;;=> (0 1 2 3 4 5 6 7 8 9)
4 (filter odd? (range 10)) ;;=> (1 3 5 7 9)
odd?
nil
en false
logisch waar)1 (reduce + [1 2 3 4 5]) ;;=> 15
Stappen:
1 (reduce + [1 2 3 4 5])
2 (+ 1 2) ;;=> 3
3 (reduce + [3 3 4 5])
4 (+ 3 3 ) ;;=> 6
5 (reduce + [6 4 5])
6 (+ 6 4) ;;=> 10
7 (reduce + [10 5])
8 (+ 10 5) ;;=> 15
9 15
Veel functies gebruiken intern reduce
, waardoor ze zijn aan te
roepen met een variabel aantal argumenten:
1 (+ 1 2 3 4 5) ;;=> 15
Complexere functie: frequencies
:
1 (frequencies ["foo" "bar" "bar" "foo" "baz"])
2 ;;=> {"foo" 2, "bar" 2, "baz" 1}
reduce
?1 (def l1 (list 1 2 3))
2 (def l2 (conj l1 0))
3 l2 ;;=> (0 1 2 3)
Structural sharing:
1 (def v [1 2 3])
2 (rest v) ;;=> (2 3)
3 (conj v 4) ;;=> [1 2 3 4]
4 (pop v) ;;=> [1 2]
5 (assoc v 0 10) ;;=> [10 3 4]
6 (subvec v 1 2) ;;=> [2]
Je kunt vectoren niet wijzigen. Wel nieuwe maken op basis van oude.
1 (def m {:a "foo", :b "bar"})
2 (get m :a) ;;=> "foo"
3 (get m :c) ;;=> nil
4 (get m :c :not-found) ;;=> :not-found
5 (assoc m :c "baz") ;;=> {:a "foo", :c "baz", :b "bar"}
1 ({:a "foo" :b "bar"} :a) ;;=> "foo"
2
3 (:b {:a "foo" :b "bar"}) ;;=> "bar"
We weten nu alle ingrediënten om zelf frequencies
te kunnen
programmeren...
1 (frequencies ["foo" "bar" "bar" "foo" "baz"])
2 ;;=> {"foo" 2, "bar" 2, "baz" 1}
reduce
?reduce
1 def frequencies(coll):
2 result = {}
3 for elt in coll:
4 if elt in result:
5 result[elt] += 1
6 else:
7 result[elt] = 1
8 return result
9
10 frequencies(["foo","bar","foo", "bar","baz"])
11 => {'bar': 2, 'baz': 1, 'foo': 2}
Maak een functie f
zodat:
1 (f {} "foo") ;;=> {"foo" 1}
2 (f {"foo" 1} "foo") ;;=> {"foo" 2}
3 (f {"foo" 1 "bar" 1} "foo") ;;=> {"foo" 2 "bar" 1}
4 (f {"foo" 1} "bar") ;;=> {"foo" 1, "bar" 1}
5
6 (defn f [m e]
7 ...)
1 (use 'clojure.test)
2 (declare f) ;; f bestaat nog niet
3 (deftest test-f
4 (is (= (f {} "foo") {"foo" 1}))
5 (is (= (f {"foo" 1} "foo") {"foo" 2}))
6 (is (= (f {"foo" 1 "bar" 1} "foo") {"foo" 2 "bar" 1}))
7 (is (= (f {"foo" 1} "bar") {"foo" 1, "bar" 1})))
8
9 (run-tests)
10 Ran 1 tests containing 4 assertions.
11 0 failures, 4 errors.
12 {:type :summary, :pass 0, :test 1, :error 4, :fail 0}
4 errors, want f
is nog niet gedefiniëerd
Maak een functie f
zodat:
1 (f {} "foo") ;;=> {"foo" 1}
2
3 (defn f [m e]
4 (if (= m {}) {e 1}))
5
6 (run-tests)
7
8 ...
1 Testing user
2
3 FAIL in (test-f) (NO_SOURCE_FILE:3)
4 expected: (= (f {"foo" 1} "foo") {"foo" 2})
5 actual: (not (= nil {"foo" 2}))
6
7 FAIL in (test-f) (NO_SOURCE_FILE:4)
8 expected: (= (f {"foo" 1, "bar" 1} "foo") {"foo" 2, "bar" 1})
9 actual: (not (= nil {"foo" 2, "bar" 1}))
10
11 FAIL in (test-f) (NO_SOURCE_FILE:5)
12 expected: (= (f {"foo" 1} "bar") {"foo" 1, "bar" 1})
13 actual: (not (= nil {"foo" 1, "bar" 1}))
14
15 Ran 1 tests containing 4 assertions.
16 3 failures, 0 errors.
17 {:type :summary, :pass 1, :test 1, :error 0, :fail 3}
Maak een functie f
zodat:
1 (is (= (f {"foo" 1} "foo") {"foo" 2}))
2
3 (contains? {:a 1 :b 2} :a) ;;=> true
4 (contains? {:a 1 :b 2} :c) ;;=> false
5
6 (defn f [m e]
7 (if (= m {}) {e 1}
8 (if (contains? m e)
9 (assoc m e (inc (m e))))))
10
11 (run-tests)
12 ...
1 Testing user
2
3 FAIL in (test-f) (NO_SOURCE_FILE:5)
4 expected: (= (f {"foo" 1} "bar") {"foo" 1, "bar" 1})
5 actual: (not (= nil {"foo" 1, "bar" 1}))
6
7 Ran 1 tests containing 4 assertions.
8 1 failures, 0 errors.
9 {:type :summary, :pass 3, :test 1, :error 0, :fail 1}
NB, de case
1 (is (= (f {"foo" 1 "bar" 1} "foo") {"foo" 2 "bar" 1}))
slaagt ook al.
1 (defn f [m e]
2 (if (= m {}) {e 1}
3 (if (contains? m e)
4 (assoc m e (inc (m e)))
5 (assoc m e 1))))
6
7 (run-tests)
8
9 Testing user
10
11 Ran 1 tests containing 4 assertions.
12 0 failures, 0 errors.
13 {:type :summary, :pass 4, :test 1, :error 0, :fail 0}
Het werkt! Nog niet helemaal 'idiomatic'. Zometeen refactoren. Nu eerst even stap 2.
1 user=> (reduce f ["foo" "bar" "bar" "foo" "baz"])
2 IllegalArgumentException contains? not supported on type:
3 java.lang.String clojure.lang.RT.contains (RT.java:724)
Dit klopt. Functie f
verwacht een map en een element (string).
1 (reduce f {} ["foo" "bar" "bar" "foo" "baz"])
2 ;;=> {"baz" 1, "bar" 2, "foo" 2}
Zelfde als:
1 (reduce f [{} "foo" "bar" "bar" "foo" "baz"])
Stappen:
1 (f {} "foo") ;;=> {"foo" 1}
2 (f {"foo" 1} "bar") ;;=> {"foo" 1 "bar" 1}
3 (f {"foo" 1 "bar" 1} "bar") ;;=> {"foo" 1 "bar" 2}
4 (f {"foo" 1 "bar" 2} "foo") ;;=> {"foo" 2 "bar" 2}
5 (f {"foo" 2 "bar" 2} "baz") ;;=> {"baz" 1, "bar" 2, "foo" 2}
De functie frequencies
:
1 (defn f [m e]
2 (if (= m {}) {e 1}
3 (if (contains? m e)
4 (assoc m e (inc (m e)))
5 (assoc m e 1))))
6
7 (defn frequencies [coll]
8 (reduce f {} coll))
De functie frequencies
:
1 (defn- f [m e] ;; private function
2 (if (= m {}) {e 1}
3 (if (contains? m e)
4 (assoc m e (inc (m e)))
5 (assoc m e 1))))
6
7 (defn frequencies [coll]
8 (reduce f {} coll))
De functie frequencies
:
1 (defn frequencies [coll]
2 (let [reducefn (fn [m e] ;; local function
3 (if (= m {}) {e 1}
4 (if (contains? m e)
5 (assoc m e (inc (m e)))
6 (assoc m e 1))))]
7 (reduce reducefn {} coll)))
De functie frequencies
:
1 (defn frequencies [coll]
2 (reduce (fn [m e] ;; anonymous function
3 (if (= m {}) {e 1}
4 (if (contains? m e)
5 (assoc m e (inc (m e)))
6 (assoc m e 1))))
7 {} coll))
De reduce-functie kan mooier/korter. Overbodige case, wie ziet 'em?
Indien lege map meegegeven:
1 (= m {}) {e 1}
Indien niet lege map en key zat nog niet in de map:
1 (assoc {"foo" 1} "bar" 1) ;;=> {"foo" 1 "bar" 1}
Maar:
1 (assoc {} "foo" 1) ;;=> {"foo" 1}
Dus 'special case' is onnodig.
1 (defn frequencies [coll]
2 (reduce (fn [m e]
3 (if (contains? m e)
4 (assoc m e (inc (m e)))
5 (assoc m e 1)))
6 {} coll))
De reduce-functie kan nóg mooier/korter. Wie heeft een suggestie?
1 (def m {"foo" 1 "bar" 2})
2 (m "foo") ;;=> 1
3 (m "bar") ;;=> 2
4 (m "baz") ;;=> nil
5 (m "baz" :not-found) ;;=> :not-found
6 (m "baz" 0) ;;=> 0
1 (defn frequencies [coll]
2 (reduce (fn [m e]
3 (assoc m e (inc (m e 0))))
4 {} coll))
1 (defn frequencies [coll]
2 (reduce (fn [m e]
3 (assoc m e (inc (m e 0))))
4 {} coll))
vs
1 def frequencies(coll):
2 result = {}
3 for elt in coll:
4 if elt in result:
5 result[elt] += 1
6 else:
7 result[elt] = 1
8 return result
1 (defn frequencies
2 "Returns a map from distinct items in coll to the number of times
3 they appear."
4 {:added "1.2"
5 :static true}
6 [coll]
7 (persistent!
8 (reduce (fn [counts x]
9 (assoc! counts x (inc (get counts x 0))))
10 (transient {}) coll)))
Transients optimalisatie, algoritme is hetzelfde.
In Eclipse:
freqweb
freqweb.repl.clj
(start-server)
In freqweb.routes.home.clj
1 (ns freqweb.routes.home
2 (:require [compojure.core :refer :all]
3 [freqweb.views.layout :as layout])
4 (:use [hiccup.form])) ;; <-
In freqweb.routes.home.clj
1 (defn home []
2 (layout/common
3 [:div
4 [:p "Please enter some text for analysis"]
5 (form-to [:post "/myresult"]
6 (text-area {:rows "4" :cols "50"} "mytext")
7 [:br]
8 (submit-button "Send"))]))
1 (defn myresult [text]
2 (let [words (clojure.string/split text #" ")
3 freqs (frequencies words)]
4 (layout/common [:p (str freqs)])))
5
6 (defroutes home-routes
7 (GET "/" [] (home))
8 (POST "/myresult" [mytext] (myresult mytext)))
Hoe tonen we bijvoorbeeld de top 5 resultaten in een mooie tabel?
Bijvoorbeeld sorteren op lengte van strings
1 (sort (fn [a b] (< (count a) (count b)))
2 ["dog" "elephant" "bird"])
3 ;;=> ("dog" "bird" "elephant")
4 (sort-by count < ["dog" "elephant" "bird"])
5 ;;=> ("dog" "bird" "elephant")
(Vraag: is sort
een hogere orde functie?)
1 (def m {"foo" 1 "bar" 2 "baz" 4 "qux" 3})
2 (seq m) ;;=> (["foo" 1] ["bar" 2] ["qux" 3] ["baz" 4])
3 (sort (fn [a b] (> (second a) (second b))) m)
4 ;;=> (["baz" 4] ["qux" 3] ["bar" 2] ["foo" 1])
5 (sort-by second > m)
6 ;;=> (["baz" 4] ["qux" 3] ["bar" 2] ["foo" 1])
7 (take 2 (sort-by second > m)) ;;=> (["baz" 4] ["qux" 3])
1 (defn select-top-5 [freqs]
2 (take 5 (sort-by second > freqs)))
1 (defn myresult [text]
2 (let [words (clojure.string/split text #" ")
3 freqs (frequencies words)
4 top5 (select-top-5 freqs)]
5 (layout/common [:p (for [[word amount] top5]
6 [:p "The word " word
7 " occurs " amount " times"])]
8 [:a {:href "/"} "Back"])))
x
Bedankt voor jullie aandacht!
Vragen?
Table of Contents | t |
---|---|
Exposé | ESC |
Full screen slides | e |
Presenter View | p |
Source Files | s |
Slide Numbers | n |
Toggle screen blanking | b |
Show/hide slide context | c |
Notes | 2 |
Help | h |