Fork me on GitHub
#braveandtrue
<
2018-09-12
>
moo00:09:31

I got another question

moo00:09:38

from: ch3 ex5.

moo00:09:40

I have this:

moo00:09:05

(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

moo00:09:46

in (defn radial-seq-part ... I’d like it to just pass the input part if that :when test fails

moo00:09:12

currently, it returns () when the string doesn’t match #"^left-"

manutter5100:09:47

Ok, let me just catch up on what’s in the B&T text for this…

moo00:09:21

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

manutter5100:09:31

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?

moo00:09:11

and in my code there attempt 1 works, and but I’m trying to be DRY

moo00:09:18

so I’m using for

moo00:09:32

and it returns the expected output for #"^left-"

moo00:09:43

it returns () for non regex matching inputs

manutter5100:09:31

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

moo00:09:49

so just if before the for

moo00:09:04

’ight. That’s not to bad. Thanks!

moo00:09:46

great it works, but it’s not flattened

moo00:09:08

(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) 

manutter5100:09:23

use mapcat instead of map

moo00:09:28

--->
({: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

moo00:09:36

I’ll check that out. ty!

moo00:09:31

mapcat flattens but the passed through parts from if are vectors not maps

moo00:09:47

([:name "head"]
 [:size 3]
 {:name "c1-eye", :size 1}
 {:name "c2-eye", :size 1}
 {:name "c3-eye", :size 1}; and so on...

moo00:09:23

I could -> flatten map

manutter5100:09:17

Try wrapping the mapcat with (into {} ...)

manutter5100:09:44

although hmm, not quite

moo00:09:03

(flatten (map … ) works

manutter5100:09:50

Does it? I thought flatten would give you something like (:name "head" :size 3 :name "c1-eye" :size 1...)

manutter5100:09:17

Oh well, I rarely use flatten, it’s probably just my ignorance

moo00:09:37

(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

moo00:09:24

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)) 

moo00:09:29

not to ugly. Thanks!

moo00:09:49

it’s the for that’s adding the extra nesting

moo01:09:01

because that’s a sequence of maps

moo01:09:07

so… it makes sense

moo01:09:13

that I need to flatten

moo01:09:34

I suppose :when is redundant

manutter5101:09:19

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

manutter5101:09:48

(and yeah, you don’t need the :when now)

moo01:09:09

tried it,… still nested

manutter5101:09:57

what’s it look like?

moo01:09:06

oh nm, you’re right

moo01:09:22

(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))

moo01:09:34

what happened there with the list?

manutter5101:09:38

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

😁 4
moo01:09:17

so mapcat has consistent types

moo01:09:36

thanks again man!

moo01:09:56

I finished the book, and now I’m doing each chapters exercises…

moo01:09:52

alright, that’s all for my day. See ya!

manutter5101:09:31

have a good one

moo13:09:58

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))

moo13:09:48

(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))

moo13:09:50

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))

manutter5113:09:25

What kind of data is mapify being called on in the original example?

moo13:09:59

and convert has keys that map to functions to parse string to int etc.

moo13:09:14

;;; 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))

manutter5113:09:16

Hmm, I think I’m following.

moo13:09:02

I’m trying to write vamp-validators, validate-entry and validate. The latter of which is like mapify

moo13:09:19

but I don’t get mapify

manutter5113:09:14

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

manutter5113:09:27

So in our example, (map vector vamp-keys unmapped-row) is going to return [[:a 1] [:b 2] [:c 3] [:d 4]]

moo13:09:13

gotcha so (map vector vamp-keys unmapped-row) is taking

[:name :glitter-index] and ["bob" "10"] and returning {:name "bob" :glitter-index "10"}

manutter5113:09:42

You have curly braces, I have square braces

moo13:09:19

[:name :glitter-index] and ["bob" "10"] and returning [:name "bob" :glitter-index "10"]

moo13:09:31

because this is input to vector

manutter5113:09:00

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

moo13:09:27

k, and this becomes input to reduce

manutter5113:09:06

Right, each of those 2-element vectors is passed in as the 2nd arg to your reducing function

manutter5113:09:17

(The first arg is the map you’re accumulating)

manutter5113:09:55

Actually, I missed a slight mistake in your value, above

manutter5113:09:35

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

manutter5113:09:58

i.e. '([:name "bob"] [:glitter-index "10"])

moo13:09:05

(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))

manutter5113:09:54

That looks good — as a purist, I’d say to create '([:name "Bob"] [:glitter-index "10"])

moo13:09:57

so I’m following along … until reduce

manutter5113:09:02

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.

manutter5113:09:36

It’s in a comment so it won’t break anything, but probably a good idea to get in the habit

manutter5113:09:52

Anyway, the reduce

manutter5113:09:50

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

manutter5113:09:42

The first time thru, it’s going to call the reducing function with {} as the first argument, and [:name "Bob"] as the 2nd argument.

moo13:09:35

oh, so that’s why row-map is our first argument in the anon fn

manutter5113:09:36

Since you’ve got destructuring going on, that’s going to assign :name to vamp-key and "Bob" to value

manutter5113:09:00

right, row-map will be {} the first time thru

moo13:09:57

somehow I don’t see how it turns into a legit map

moo13:09:03

it seems like this ’ought to happen:

moo13:09:26

{:name “Bob”} {:glitter-index 10}

manutter5113:09:05

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.

moo13:09:21

(def conversions {:name identity :glitter-index str->int})

(defn convert
  [vamp-key value]
  ((get conversions vamp-key) value))

moo13:09:38

it’s just type casting the number

manutter5113:09:39

Ok, cool, that’s what I expected

moo13:09:42

based on the key

manutter5113:09:30

So now inside the function, you’re calling (assoc row-map :name "Bob") (once convert is done converting it)

manutter5113:09:46

and that’s going to return {:name "Bob"}

moo13:09:53

I’m with ya

manutter5113:09:21

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.

manutter5113:09:00

In other words, the 2nd time, we’re going to call the function with the arguments {:name "Bob"} and [:glitter-index "10"]

moo13:09:21

then, I’m imagining {:name “bob” :gl 2 :name “otherguy” :gl 3} which diesnt make sense

manutter5113:09:51

Yeah, it’s a little tricky, but that’s not what’s happening

moo13:09:05

what happens the 3rd time?

manutter5113:09:22

For each row, you only have each key once

manutter5113:09:47

:name "otherguy" is the next row

manutter5113:09:28

so that’s not part of the list that gets passed to the reduce — it’s a reduce nested inside a map

moo13:09:13

we’re just reducing two entries

moo13:09:23

then it’s back to getting mapped onto a new row

manutter5113:09:43

Well, if there’s only 2 entries per row, yes

moo13:09:58

reduce is operating on only one row

moo13:09:14

and map is applying reduce row-by-row

manutter5113:09:18

It would be easier to follow if you pulled the inner reduce out into its own function called convert-row-to-map or something

manutter5113:09:06

Then mapify would just be (defn mapify [rows] (map convert-row-to-map rows))

manutter5113:09:30

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.

manutter5113:09:03

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.

moo13:09:18

that makes sense. 🙂

moo13:09:44

so, I can imagine a few ways to make validators, but they don’t seem very slick

moo13:09:48

for example

moo13:09:31

I could just do ifs for name and glitter-index for an entry, then use map to apply it

moo13:09:46

however, I want this to return false right away

moo13:09:22

anyways, I ’ll go puzzle it out 🙂

moo13:09:31

thanks again man, that really helped!

moo14:09:28

is there an non macro version of and

moo14:09:45

trying to (apply and (false true false true))

moo14:09:10

if I use map with validation, I’ll get a lazy seq of booleans

moo14:09:55

I guess I can use some

manutter5114:09:34

there’s also every?

manutter5114:09:23

That’s probably what you want instead of apply and

moo14:09:30

this is what I came up with

moo14:09:35

(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))))

moo14:09:49

cool, every is what I needed

moo14:09:13

(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)))

manutter5114:09:40

Looks good — there’s some optimizations you can apply

manutter5114:09:45

You can replace (not-empty some-string) with (seq some-string)

manutter5114:09:56

(seq "a")
=> (\a)
(seq "")
=> nil

manutter5114:09:56

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

manutter5114:09:39

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