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)
There must me some difference between {} and ({})
That's part of it. The first argument to conj
is the collection you want to append something to.
and the second is the thing you want to add.
Is {:name "Moo" :glitter-index 5}
not a collection?
No, it's a map
There are some contexts where you can treat a map as though it were a coll, but conj
isn't one of them.
so many maps is a collection, but a single mapping is not
Right
So, the error persistentArrayMap is telling me I need this [{stuffs}]
?
Great everything makes sense
(into [{:name "Moo" :glitter-index 5}] initial-suspects)
works
Yes, that'll work too
conj doesn’t work in the above case because it makes a collection of two collections
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
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.
makes sense
Thank you again @manutter51 ~
!
Anytime
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.)
k. I found the coll?
function. 🙂
That’ll help
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]]
?What is the empty map doing in there?
0_o
The empty map is for the reduce
function to use as a starting value.
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
Ok, back.
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.
Can you destructure maps by specific keys?
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] ]
I caught that part
Yes, you can destructure maps by specific keys too
Ok, so that’s what’s going on with the function inside the reduce
Did I answer your question about the empty map arg?
like: (fn [my-map-type [name address]] (clojure.string/join ", " [name address]))
off to the repl!
yeah, I don’t know how to pull keys by value in the destructuring
(map :somekey my-map)
works, but how does one do that work destructuring
just a sec, just got pulled in on another call
I found this: https://clojure.org/guides/destructuring#_associative_destructuring
(fn [{name :name}] (str name) (take 1 initial-suspects))
returns some object reference…
; ==> #object[fwpd.core$eval2099$fn__2101 0x3c47f026 "fwpd.core$eval2099$fn__2101@3c47f026"]
Yeah, that’s the official reference. What I find easiest is just to do the (let [{:keys [name1 name2]} my-map] ...)
Or for a function it would look like (fn [{:keys [name]}] (str name) ...)
strange that is going to grab the values
Yeah, that is totally an idiom. Or “syntactic sugar” if you prefer
actually, I’m getting the same problem
(fn [{:keys [name]}] (str name) (take 1 initial-suspects))
fwpd.core=>
#object[fwpd.core$eval2165$fn__2167 0x15e29d5c "fwpd.core$eval2165$fn__2167@15e29d5c"]
with
(take 1 initial-suspects)
fwpd.core=>
({:name "Edward Cullen", :glitter-index 10})
You’re close, but you’re putting the (take 1 initial suspects)
inside the fn
oh
You want it to be the value you pass to the fn
I see
one sec
((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)
well, I can tell I’m going to get good a paren matching. Haha
heh
((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)
(defn try-it (fn [{:keys [name]}] (println "what will happen now?")))
IllegalArgumentException Parameter declaration "fn" should be a vector
oh, ok, yeah, (take 1 initial-suspects)
is returning a collection of maps, not just a map. Try (first initial-suspects)
so something is wrong with my anonymous function definition
works!
:thumbsup:
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"
yay. Thanks @manutter51!
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.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.
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.
is it that I destructure immediately?
before the fn call?
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.
gotcha
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.
like loop based towers of hanoi is better than recursion
Right, tail-call recursion is ok IF you have the optimization that re-uses the stack frame instead of piling up on the stack.
(which Clojure does by using loop
/`recur`)
maybe after ch5 I”ll try to do towers of hanoi with loop recur
memoize
is mega-handy!
😄
It is pretty cool
I wonder how long it’s going to take to get to being comfortable to build a react app in re-frame
I’m betting 8 weeks
Possibly, maybe more maybe less. I’m still learning nuances and stuff and I’ve been using it for a good while now
I have a sneaking feeling that clojure is going to wind up being like mathematica
that’s the closest to lisp I’ve ever come
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.
Just the fact that Clojure uses []
and {}
in a sensible way, instead of ()
everywhere, makes a HUGE difference to me
more visual cues as to the context
the other lisps don’t have [] or {}
?
yeah that’d be harder
Not that I’m aware of. Everything is parens.
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
but that sounds like code as data
anyhoo back to it 🙂