Fork me on GitHub
#braveandtrue
<
2018-08-24
>
moo13:08:44

Hi All, I have a question about Ch4 ex 2. - “Write an append function for suspects” For some reason one version of my code works and one does not. I don’t understand the difference. Starting from here:

(def initial-suspects (mapify (parse (slurp filename))))
initial-suspects
;; => ({:name "Edward Cullen", :glitter-index 10} {:name "Bella Swan", :glitter-index 0} {:name "Charlie Swan", :glitter-index 0} {:name "Jacob Black", :glitter-index 3} {:name "Carlisle Cullen", :glitter-index 6})
This works:
(conj initial-suspects {:name "Moo" :glitter-index 5})
And this does not:
(conj  {:name "Moo" :glitter-index 5} initial-suspects)
;; ==> ClassCastException clojure.lang.PersistentArrayMap cannot be cast to java.base/java.util.Map$Entry  clojure.lang.APersistentMap.cons (APersistentMap.java:42)

moo13:08:52

There must me some difference between {} and ({})

manutter5113:08:48

That's part of it. The first argument to conj is the collection you want to append something to.

manutter5113:08:05

and the second is the thing you want to add.

moo13:08:33

Is {:name "Moo" :glitter-index 5} not a collection?

manutter5113:08:40

No, it's a map

manutter5113:08:06

There are some contexts where you can treat a map as though it were a coll, but conj isn't one of them.

moo13:08:07

so many maps is a collection, but a single mapping is not

moo13:08:54

So, the error persistentArrayMap is telling me I need this [{stuffs}]

moo13:08:38

Great everything makes sense

moo13:08:48

(into [{:name "Moo" :glitter-index 5}] initial-suspects) works

manutter5113:08:11

Yes, that'll work too

moo13:08:43

conj doesn’t work in the above case because it makes a collection of two collections

manutter5113:08:50

Partly because into is smarter about pulling one collection into another, but also because you've got square brackets around the map, making it a collection of maps

manutter5113:08:50

The reason conj doesn’t work is because it’s designed to take a list of things, and “conjoin” one more thing onto the list.

moo13:08:01

makes sense

moo13:08:23

Thank you again @manutter51 ~

manutter5113:08:21

I should mention that there are some circumstances under which you can have a map as the first arg to conj, but for now it’s probably better to just assume you shouldn’t do that. (It’s a little complicated to explain how and why it can work.)

moo13:08:46

k. I found the coll? function. 🙂

moo13:08:50

That’ll help

moo13:08:41

Another Q, regarding destructuring. What is happening here with new-map?

(reduce (fn [new-map [key val]]
          (assoc new-map key (inc val)))
        {}
        {:max 30 :min 10})
; => {:max 31, :min 11}
It like this? [some-collection [destructuring-some-collection1… n]]?

moo13:08:31

What is the empty map doing in there?

manutter5113:08:15

The empty map is for the reduce function to use as a starting value.

manutter5114:08:17

I’m just getting pulled in to a meeting here, I’ll be back in 20-30 min and I can go into more detail

manutter5114:08:46

There’s one weird thing that Clojure does with maps that I thought was a little confusing when I was a beginner, and that’s how maps behave with iterators like reduce and map and filter and so on.

moo14:08:31

Can you destructure maps by specific keys?

manutter5114:08:05

Basically, when you iterate over a map, Clojure pretends it’s really an array of 2-element arrays, so like this:

{:max 30 :min 10} ;; ==> [ [:max 30]
                  ;;       [:min 10] ]

moo14:08:21

I caught that part

manutter5114:08:33

Yes, you can destructure maps by specific keys too

manutter5114:08:53

Ok, so that’s what’s going on with the function inside the reduce

manutter5114:08:35

Did I answer your question about the empty map arg?

moo14:08:49

like: (fn [my-map-type [name address]] (clojure.string/join ", " [name address]))

moo14:08:23

off to the repl!

moo14:08:53

yeah, I don’t know how to pull keys by value in the destructuring

moo14:08:18

(map :somekey my-map) works, but how does one do that work destructuring

manutter5114:08:33

just a sec, just got pulled in on another call

moo14:08:15

(fn [{name :name}] (str name) (take 1 initial-suspects)) returns some object reference…

; ==> #object[fwpd.core$eval2099$fn__2101 0x3c47f026 "[email protected]"]

manutter5114:08:41

Yeah, that’s the official reference. What I find easiest is just to do the (let [{:keys [name1 name2]} my-map] ...)

manutter5114:08:31

Or for a function it would look like (fn [{:keys [name]}] (str name) ...)

moo14:08:59

strange that is going to grab the values

manutter5114:08:26

Yeah, that is totally an idiom. Or “syntactic sugar” if you prefer

moo14:08:39

actually, I’m getting the same problem

moo14:08:52

(fn [{:keys [name]}] (str name) (take 1 initial-suspects))
fwpd.core=>
#object[fwpd.core$eval2165$fn__2167 0x15e29d5c "[email protected]"]

moo14:08:10

with

(take 1 initial-suspects)
fwpd.core=>
({:name "Edward Cullen", :glitter-index 10})

manutter5114:08:39

You’re close, but you’re putting the (take 1 initial suspects) inside the fn

manutter5114:08:57

You want it to be the value you pass to the fn

moo14:08:45

((fn [{:keys [name]}] (str name)) (take 1 initial-suspects))
IllegalArgumentException No value supplied for key: {:name "Edward Cullen", :glitter-index 10}  clojure.lang.PersistentHashMap.create (PersistentHashMap.java:77)

moo14:08:27

well, I can tell I’m going to get good a paren matching. Haha

moo14:08:05

((fn [{:keys [name]}] (println "what will happen now?")) (take 1 initial-suspects))
IllegalArgumentException No value supplied for key: {:name "Edward Cullen", :glitter-index 10}  clojure.lang.PersistentHashMap.create (PersistentHashMap.java:77)

moo14:08:22

(defn try-it (fn [{:keys [name]}] (println "what will happen now?")))
IllegalArgumentException Parameter declaration "fn" should be a vector 

manutter5114:08:32

oh, ok, yeah, (take 1 initial-suspects) is returning a collection of maps, not just a map. Try (first initial-suspects)

moo14:08:38

so something is wrong with my anonymous function definition

moo14:08:28

well I don’t know if this is idomatic, but here is what I ended up with for problem 4.

(defn csv-line [{:keys [name glitter-index]}] (str (clojure.string/join "," [name glitter-index]) "\n"))
(reduce str (map csv-line initial-suspects))
;;==>  "Edward Cullen,10\nBella Swan,0\nCharlie Swan,0\nJacob Black,3\nCarlisle Cullen,6\n"

moo14:08:54

yay. Thanks @manutter51!

manutter5115:08:32

By the way, as a stylistic issue, when I’m writing application code, I prefer to do something like this:

(defn csv-line [m]
  (let [{:keys [name glitter-index]} m]
  ...)
In other words, I don’t do destructuring in function arguments, I take the full map as an argument, and then use let inside the fn to do the destructuring. The reason I like that better is because if this is a production app, I know that some day I’m going to come back to this function because somewhere I made a change, and this code broke. By accepting the whole map as an argument, I can sneak in a line just before the let that says (prn m), as a debugging step. Is a key missing? Is it present, but somehow set to null? I’ve got the whole map coming in, and I can dump it to the console and see exactly what’s different.

manutter5115:08:33

There have been times I’ve done this and discovered that somehow this fn was being called with the wrong map entirely! Much easier to discover when you have the whole map to work with instead of just some subset of the values.

moo15:08:19

In your code above I see that you take in the map m and destructure in the let scope. That makes sense. I can see how that “looks better.” I also see how I didn’t take in a general map m, but I didn’t understand how my code is more brittle.

moo15:08:01

is it that I destructure immediately?

moo15:08:18

before the fn call?

manutter5115:08:58

The destructuring happens in the process of evaluating the arguments to your function (which is getting pretty technical, so you don’t need to worry to much about that). I should also add that it’s not at all uncommon to see professional Clojurists putting the destructuring right in the function args, just like your code does. It’s perfectly legitimate, and my style is purely a personal preference.

moo15:08:18

As a side note, before looking into FP, I never knew about tail-call recursion being okay. I was taught to avoid recursion and so as not to worry about stack overflow.

moo15:08:45

like loop based towers of hanoi is better than recursion

manutter5115:08:17

Right, tail-call recursion is ok IF you have the optimization that re-uses the stack frame instead of piling up on the stack.

manutter5115:08:39

(which Clojure does by using loop/`recur`)

moo15:08:09

maybe after ch5 I”ll try to do towers of hanoi with loop recur

moo15:08:33

memoize is mega-handy!

manutter5115:08:44

It is pretty cool

moo15:08:57

I wonder how long it’s going to take to get to being comfortable to build a react app in re-frame

moo15:08:03

I’m betting 8 weeks

manutter5115:08:59

Possibly, maybe more maybe less. I’m still learning nuances and stuff and I’ve been using it for a good while now

moo15:08:11

I have a sneaking feeling that clojure is going to wind up being like mathematica

moo15:08:18

that’s the closest to lisp I’ve ever come

manutter5115:08:55

Yeah, I tried multiple times to get comfortable with a Lisp, but Clojure’s the one that made it easy enough that I could actually use it.

manutter5115:08:34

Just the fact that Clojure uses [] and {} in a sensible way, instead of () everywhere, makes a HUGE difference to me

moo15:08:01

more visual cues as to the context

moo15:08:21

the other lisps don’t have [] or {}

moo15:08:28

yeah that’d be harder

manutter5115:08:49

Not that I’m aware of. Everything is parens.

moo15:08:20

mathematica has funny search and replace, which you can use on your code if you suspend evaluation. But, … I never wrote anything you’d call a program. It’s more like, simulate something or calculation something

moo15:08:35

but that sounds like code as data

moo15:08:42

anyhoo back to it 🙂