hi @manutter51
I got another question
from: ch3 ex5.
I have this:
(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
currently, it returns () when the string doesn’t match #"^left-"
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.
;;;
Thanks!
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?
yep
and in my code there attempt 1 works, and but I’m trying to be DRY
so I’m using for
and it returns the expected output for #"^left-"
but
it returns () for non regex matching inputs
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
so just if
before the for
right
’ight. That’s not to bad. Thanks!
:thumbsup:
great it works, but it’s not flattened
(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
I’ll check that out. ty!
mapcat flattens but the passed through parts from if are vectors not maps
([:name "head"]
[:size 3]
{:name "c1-eye", :size 1}
{:name "c2-eye", :size 1}
{:name "c3-eye", :size 1}; and so on...
I could -> flatten map
Try wrapping the mapcat with (into {} ...)
although hmm, not quite
(flatten (map … )
works
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))
not to ugly. Thanks!
it’s the for
that’s adding the extra nesting
because that’s a sequence of maps
so… it makes sense
that I need to flatten
I suppose :when
is redundant
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)
tried it,… still nested
what’s it look like?
oh nm, you’re right
(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))
what happened there with the list?
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
gotcha
so mapcat has consistent types
thanks again man!
I finished the book, and now I’m doing each chapters exercises…
cool
alright, that’s all for my day. See ya!
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?
A CSV
and convert has keys that map to functions to parse string to int etc.
;;; 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
but I don’t get 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
[:name :glitter-index] and ["bob" "10"] and returning [:name "bob" :glitter-index "10"]
because this is input to vector
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
k, and this becomes input to reduce
…
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"])
so I’m following along … until reduce
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.
I see
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.
oh, so that’s why row-map
is our first argument in the anon fn
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
somehow I don’t see how it turns into a legit map
it seems like this ’ought to happen:
{:name “Bob”} {:glitter-index 10}
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))
it’s just type casting the number
Ok, cool, that’s what I expected
based on the key
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"}
I’m with ya
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"]
then, I’m imagining {:name “bob” :gl 2 :name “otherguy” :gl 3} which diesnt make sense
Yeah, it’s a little tricky, but that’s not what’s happening
what happens the 3rd time?
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
oh!
we’re just reducing two entries
then it’s back to getting mapped onto a new row
Well, if there’s only 2 entries per row, yes
reduce is operating on only one row
Right
and map is applying reduce row-by-row
a ha
great!
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))
yes
true
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.
that makes sense. 🙂
so, I can imagine a few ways to make validators, but they don’t seem very slick
for example
I could just do ifs for name and glitter-index for an entry, then use map to apply it
however, I want this to return false right away
anyways, I ’ll go puzzle it out 🙂
thanks again man, that really helped!
:thumbsup:
is there an non macro version of and
trying to (apply and (false true false true))
if I use map
with validation, I’ll get a lazy seq of booleans
I guess I can use some
there’s also every?
That’s probably what you want instead of apply and
great
this is what I came up with
(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))))
cool, every is what I needed
(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. 🙂