This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2018-09-12
Channels
- # beginners (63)
- # boot (3)
- # braveandtrue (153)
- # cider (19)
- # cljdoc (2)
- # clojure (80)
- # clojure-dev (25)
- # clojure-italy (73)
- # clojure-losangeles (1)
- # clojure-nl (4)
- # clojure-spec (67)
- # clojure-sweden (1)
- # clojure-uk (83)
- # clojurescript (56)
- # clojutre (11)
- # core-logic (37)
- # cursive (18)
- # datomic (14)
- # editors (10)
- # emacs (13)
- # figwheel-main (11)
- # fulcro (62)
- # graphql (11)
- # jobs (3)
- # klipse (1)
- # leiningen (6)
- # off-topic (91)
- # onyx (7)
- # pedestal (3)
- # portkey (5)
- # re-frame (14)
- # reagent (13)
- # remote-jobs (1)
- # shadow-cljs (111)
- # tools-deps (4)
- # yada (10)
(def asym-hobbit-body-parts [{:name "head" :size 3}
{:name "left-eye" :size 1}
{:name "left-ear" :size 1}
{:name "mouth" :size 1}
{:name "nose" :size 1}
{:name "neck" :size 2}
{:name "left-shoulder" :size 3}
{:name "left-upper-arm" :size 3}
{:name "chest" :size 10}
{:name "back" :size 10}
{:name "left-forearm" :size 3}
{:name "abdomen" :size 6}
{:name "left-kidney" :size 1}
{:name "left-hand" :size 2}
{:name "left-knee" :size 2}
{:name "left-thigh" :size 4}
{:name "left-lower-leg" :size 3}
{:name "left-achilles" :size 1}
{:name "left-foot" :size 2}])
; 1st attempt works, but it ugly and repetitive
(reduce (fn [acc part]
(let [n1 {:name (clojure.string/replace (:name part) #"^left-" "c1-")
:size (:size part)}
n2 {:name (clojure.string/replace (:name part) #"^left-" "c2-")
:size (:size part)}]
(conj acc n1 n2))) [] asym-hobbit-body-parts)
; 2nd attempt, close but doesnt work
(require '[clojure.string :as cs])
(defn radial-seq-part [part]
"Given a part name, provide a lazy sequence of c1-name ... c5-name strings"
(for [c-number (range 1 6)
:let [new-name
{:name (cs/replace (:name part)
#"^left-" (str "c" c-number "-"))
:size (:size part)}]
:when (re-find #"^left-" (:name part))]
new-name))
(map radial-seq-part asym-hobbit-body-parts) ;> does c1...c5, but omits others
; 3rd attempt
in (defn radial-seq-part ...
I’d like it to just pass the input part
if that :when
test fails
Ok, let me just catch up on what’s in the B&T text for this…
;;;
; Problem 5
; Create a function that's similar to symmetrize-body-parts except that it has to
; work with weird space aliens with radial symmetry. Instead of two eyes, arms,
; legs, and so on, they have five.
;
; Create a function that generalizes symmetrize-body-parts and the function you
; created in Exercise 5. The new function should take a collection of body parts
; and the number of matching body parts to add. If you're completely new to Lisp
; languages and functional programming, it probably won't be obvious how to do
; this. If you get stuck, just move on to the next chapter and revisit the problem
; later.
;;;
Ok, I skimmed kinda quickly, the goal is to take a list of body parts, and return c1..c5 for all the “left” body parts?
I think I would probably use an if
so if it starts with “left-” then return the c1..c5, otherwise return a vector with just the original part
right
:thumbsup:
(defn radial-seq-part [part]
"Given a part, provide a lazy sequence of c1-name
... c5-name strings"
(if (re-find #"^left-" (:name part))
(for [c-number (range 1 6)
:let [new-name
{:name (cs/replace (:name part)
#"^left-" (str "c" c-number "-"))
:size (:size part)}]
:when (re-find #"^left-" (:name part))]
new-name)
part))
(map radial-seq-part asym-hobbit-body-parts)
use mapcat instead of map
--->
({:name "head", :size 3}
({:name "c1-eye", :size 1}
{:name "c2-eye", :size 1}
{:name "c3-eye", :size 1}
{:name "c4-eye", :size 1}
{:name "c5-eye", :size 1})
({:name "c1-ear", :size 1}
{:name "c2-ear", :size 1}
{:name "c3-ear", :size 1}
{:name "c4-ear", :size 1}
{:name "c5-ear", :size 1})
{:name "mouth", :size 1}
{:name "nose", :size 1}
{:name "neck", :size 2}
({:name "c1-shoulder", :size 3} ; and so on
([:name "head"]
[:size 3]
{:name "c1-eye", :size 1}
{:name "c2-eye", :size 1}
{:name "c3-eye", :size 1}; and so on...
Try wrapping the mapcat with (into {} ...)
although hmm, not quite
Does it? I thought flatten
would give you something like (:name "head" :size 3 :name "c1-eye" :size 1...)
Oh well, I rarely use flatten, it’s probably just my ignorance
(def c1to5-body-parts
(-> (map radial-seq-part asym-hobbit-body-parts)
flatten))
; ==>
({:name "head", :size 3}
{:name "c1-eye", :size 1}
{:name "c2-eye", :size 1}
{:name "c3-eye", :size 1}
{:name "c4-eye", :size 1}
{:name "c5-eye", :size 1}
{:name "c1-ear", :size 1}
{:name "c2-ear", :size 1}
{:name "c3-ear", :size 1}
{:name "c4-ear", :size 1}
{:name "c5-ear", :size 1} and so on
awesome
so my sol’n is
(require '[clojure.string :as cs])
(defn radial-seq-part [part]
"Given a part, provide a lazy sequence of c1-name
... c5-name strings"
(if (re-find #"^left-" (:name part)) ; if it's a left- string
(for [c-number (range 1 6) ; then return a lazy seqence
:let [new-name
{:name (cs/replace (:name part)
#"^left-" (str "c" c-number "-"))
:size (:size part)}]
:when (re-find #"^left-" (:name part))]
new-name)
part)) ; otherwise return the original part
(def c1to5-body-parts
(-> (map radial-seq-part asym-hobbit-body-parts)
flatten))
There’s one other thing you might try: on the line that says “otherwise return the original part”, instead of returning part
return (list part)
. Then the unchanged parts will be inside a list just like the radial parts. Then go back to mapcat instead of map/flatten
(and yeah, you don’t need the :when
now)
what’s it look like?
(defn radial-seq-part [part]
"Given a part, provide a lazy sequence of c1-name
... c5-name strings"
(if (re-find #"^left-" (:name part)) ; if it's a left- string
(for [c-number (range 1 6) ; then return a lazy seqence
:let [new-name
{:name (cs/replace (:name part)
#"^left-" (str "c" c-number "-"))
:size (:size part)}]
:when (re-find #"^left-" (:name part))] ;:when is redundant inside if
new-name)
(list part))) ; otherwise return the original part
(def c1to5-body-parts
(mapcat radial-seq-part asym-hobbit-body-parts))
So the idea is that for
returns a list and part
isn’t a list. So just wrap it in its own list, and voila, everything is a list
have a good one
Can you help me understand what’s happening here? https://www.braveclojure.com/core-functions-in-depth/#A_Vampire_Data_Analysis_Program_for_the_FWPD
(defn mapify
"Return a seq of maps like {:name \"Edward Cullen\" :glitter-index 10}"
[rows]
(map (fn [unmapped-row]
(reduce (fn [row-map [vamp-key value]]
(assoc row-map vamp-key (convert vamp-key value)))
{}
(map vector vamp-keys unmapped-row)))
rows))
(defn mapify
"Return a seq of maps like {:name \"Edward Cullen\" :glitter-index 10}"
[rows]
(map (fn [unmapped-row] ; this anonymous fn is mapped on all rows
(reduce (fn [row-map [vamp-key value]] ; ???
(assoc row-map vamp-key (convert vamp-key value)))
{}
(map vector vamp-keys unmapped-row)))
rows))
I’m working on ch. 4, ex. 3. where I’m supposed to create a similar set up for validation. However I don’t get this mapify
This is what I have so far:
(def vamp-validators
{:name #(not-empty %)
:glitter-index #(> % 0)})
(defn validate-entry
[vamp-key value]
((get vamp-validators vamp-key) value))
What kind of data is mapify
being called on in the original example?
;;; Set up
; use absolute path to get around any issues with working directory.
(def filename "/Users/moo/Sync/braveandtrue/by-chapter/ch04-suspects.csv")
(def vamp-keys [:name :glitter-index])
(defn str->int
[str]
(Integer. str))
(def conversions {:name identity :glitter-index str->int})
(defn convert
[vamp-key value]
((get conversions vamp-key) value))
(defn parse
"Convert a CSV file into rows of columns"
[string]
(map #(clojure.string/split % #",")
(clojure.string/split string #"\n")))
(defn mapify
"Return a seq of maps like {:name \"Edward Cullen\" :glitter-index 10}"
[rows]
(map (fn [unmapped-row]
(reduce (fn [row-map [vamp-key value]]
(assoc row-map vamp-key (convert vamp-key value)))
{}
(map vector vamp-keys unmapped-row)))
rows))
(defn glitter-filter
[minimum-glitter records]
(filter #(>= (:glitter-index %) minimum-glitter) records))
(parse (slurp filename))
(def glitter-map
(-> (slurp filename)
parse
mapify))
Hmm, I think I’m following.
I’m trying to write vamp-validators
, validate-entry
and validate
. The latter of which is like mapify
So for simplicity, let’s assume that vamp-keys
is [:a :b :c :d]
and we have a row that’s [1 2 3 4]
. Calling map with multiple collections just takes an entry from each collection and passes the entries to whatever function you’re mapping with, in this case vector
So in our example, (map vector vamp-keys unmapped-row)
is going to return [[:a 1] [:b 2] [:c 3] [:d 4]]
gotcha so (map vector vamp-keys unmapped-row)
is taking
[:name :glitter-index] and ["bob" "10"] and returning {:name "bob" :glitter-index "10"}
Not quite
You have curly braces, I have square braces
Right. You’re getting :key value
, which looks like it should be in a map, but in this case we’re mapping vector
over the entries, so we are just getting back a vector with a pair of entries
Right, each of those 2-element vectors is passed in as the 2nd arg to your reducing function
(The first arg is the map you’re accumulating)
Actually, I missed a slight mistake in your value, above
The map function isn’t going to return [:name "bob" :glitter-index "10"]
, it’s going to give you a list of 2-element vectors
i.e. '([:name "bob"] [:glitter-index "10"])
(defn mapify
"Return a seq of maps like {:name \"Edward Cullen\" :glitter-index 10}"
[rows]
(map (fn [unmapped-row]
; reduce an anonymous function over our list of two element vectors
; ([:name "Bob"] [:glitter-index "10"])
(reduce (fn [row-map [vamp-key value]]
(assoc row-map vamp-key (convert vamp-key value)))
{} ; reduce starts with an empty map
; combine [:name :glitter-index] with input csv row data e.g. ["Bob" "10"]
; to create ([:name "Bob"] [:glitter-index "10"])
(map vector vamp-keys unmapped-row)))
rows))
That looks good — as a purist, I’d say to create '([:name "Bob"] [:glitter-index "10"])
without the single quote, it implies that you’re trying to call [:name "Bob"]
as a function and passing [:glitter-index "10"]
as an argument.
It’s in a comment so it won’t break anything, but probably a good idea to get in the habit
Anyway, the reduce
You have {}
as the initial value for what you’re trying to accumulate, and you’ve got a list like '([:name "Bob"] [:glitter-index "10"] ...)
to iterate over
The first time thru, it’s going to call the reducing function with {}
as the first argument, and [:name "Bob"]
as the 2nd argument.
Since you’ve got destructuring going on, that’s going to assign :name
to vamp-key
and "Bob"
to value
right, row-map
will be {}
the first time thru
I assume convert
is just looking up the key to do things like “Ok, :name should be a string, just return it, but :glitter-index should be a number, so parse the string and return the actual number” etc.
(def conversions {:name identity :glitter-index str->int})
(defn convert
[vamp-key value]
((get conversions vamp-key) value))
Ok, cool, that’s what I expected
So now inside the function, you’re calling (assoc row-map :name "Bob")
(once convert
is done converting it)
and that’s going to return {:name "Bob"}
But this is reduce
, not map
, so that means the next time through, the first argument is going to be the result of the last time thru.
In other words, the 2nd time, we’re going to call the function with the arguments {:name "Bob"}
and [:glitter-index "10"]
Yeah, it’s a little tricky, but that’s not what’s happening
For each row, you only have each key once
:name "otherguy"
is the next row
so that’s not part of the list that gets passed to the reduce — it’s a reduce nested inside a map
Well, if there’s only 2 entries per row, yes
Right
It would be easier to follow if you pulled the inner reduce out into its own function called convert-row-to-map
or something
Then mapify
would just be (defn mapify [rows] (map convert-row-to-map rows))
Incidently, that’s a coding style I call “executable documentation” — when you read mapify
, you know exactly what it’s doing because it says it’s converting rows to maps.
So sometimes it’s worth pulling out the inner stuff into a named function, because the name of the fn is good documentation for what it is you’re trying to do.
:thumbsup:
there’s also every?
That’s probably what you want instead of apply and
(def vamp-validators
{:name #(if (not-empty %) true false)
:glitter-index #(>= % 0)})
(defn validate-element
[vamp-key value]
((get vamp-validators vamp-key) value))
(defn validate-entry [entry]
(not (some #(= false %)
(for [foundkey (keys entry)]
(validate-element foundkey (foundkey entry))))))
; ((get vamp-validators foundkey) (foundkey entry))))))
(defn validate [entries]
(not #(some #(= false %) (map validate-entry entries))))
(def vamp-validators
{:name #(if (not-empty %) true false)
:glitter-index #(>= % 0)})
(defn validate-element
[vamp-key value]
((get vamp-validators vamp-key) value))
(defn validate-entry [entry]
(every? true?
(for [foundkey (keys entry)]
(validate-element foundkey (foundkey entry)))))
(defn validate [entries]
(every? true? (map validate-entry entries)))
Looks good — there’s some optimizations you can apply
You can replace (not-empty some-string)
with (seq some-string)
(seq "a")
=> (\a)
(seq "")
=> nil
You can also replace every? true?
just every?
if you re-work your code a bit — there’s an implicit map
inside every?
, so you can do (every? validate-entry entries)
for example
You can do the same sort of thing to get rid of the for
, but it’s a bit tricker. Maybe you’d like to work that one out for yourself. 🙂