Reputation: 541
I'm currently reading "Clojure for the Brave and True" And in the current chapter they're explaining a program that takes a vector of hash-maps representing the body parts of a hobbit. Since the list with the parts is only provided asymmetrically (only the left arm, left eye, etc. are part of it) it was necessary to write a function that would add the corresponding right parts. Later there was an exercise to expand this function to take a number and add that number of body parts for each left one. The second function would randomly chose a body part.
This my code:
(ns clojure-noob.core
(:gen-class)
(:require [clojure.string :as str] ))
(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}])
(defn make-sym-parts [asym-set num]
(reduce (fn [sink, {:keys [name size] :as body_part}]
(if (str/starts-with? name "left-")
(into sink [body_part
(for [i (range num)]
{:name (str/replace name #"^left" (str i))
:size size})])
(conj sink body_part)))
[]
asym-set))
(defn rand-part [parts]
(def size-sum (reduce + (map :size parts)))
(def thresh (rand size-sum))
(loop [[current & remaining] parts
sum (:size current)]
(if (> sum thresh)
(:name current)
(recur remaining (+ sum (:size (first remaining)))))))
(defn -main
"I don't do a whole lot ... yet."
[arg]
(cond
(= arg "1") (println (make-sym-parts asym-hobbit-body-parts 3))
(= arg "2") (println (rand-part asym-hobbit-body-parts))
(= arg "3") (println (rand-part (make-sym-parts asym-hobbit-body-parts 3)))))
So
lein run 1
works and prints out the expanded vector
lein run 2
also works and prints out a random body part name.
BUT:
lein run 3
will produce the following error:
861 me@ryzen-tr:~/clojure_practice/clojure-noob$ lein run 3
862 Exception in thread "main" Syntax error compiling at (/tmp/form-init15519101999846500993.clj:1:74).
863 at clojure.lang.Compiler.load(Compiler.java:7647)
864 at clojure.lang.Compiler.loadFile(Compiler.java:7573)
865 at clojure.main$load_script.invokeStatic(main.clj:452)
866 at clojure.main$init_opt.invokeStatic(main.clj:454)
867 at clojure.main$init_opt.invoke(main.clj:454)
868 at clojure.main$initialize.invokeStatic(main.clj:485)
869 at clojure.main$null_opt.invokeStatic(main.clj:519)
870 at clojure.main$null_opt.invoke(main.clj:516)
871 at clojure.main$main.invokeStatic(main.clj:598)
872 at clojure.main$main.doInvoke(main.clj:561)
873 at clojure.lang.RestFn.applyTo(RestFn.java:137)
874 at clojure.lang.Var.applyTo(Var.java:705)
875 at clojure.main.main(main.java:37)
876 Caused by: java.lang.NullPointerException
877 at clojure.lang.Numbers.ops(Numbers.java:1068)
878 at clojure.lang.Numbers.add(Numbers.java:153)
879 at clojure.core$_PLUS_.invokeStatic(core.clj:992)
880 at clojure.core$_PLUS_.invoke(core.clj:984)
881 at clojure.lang.ArrayChunk.reduce(ArrayChunk.java:63)
882 at clojure.core.protocols$fn__8139.invokeStatic(protocols.clj:136)
883 at clojure.core.protocols$fn__8139.invoke(protocols.clj:124)
884 at clojure.core.protocols$fn__8099$G__8094__8108.invoke(protocols.clj:19)
885 at clojure.core.protocols$seq_reduce.invokeStatic(protocols.clj:27)
886 at clojure.core.protocols$fn__8131.invokeStatic(protocols.clj:75)
887 at clojure.core.protocols$fn__8131.invoke(protocols.clj:75)
888 at clojure.core.protocols$fn__8073$G__8068__8086.invoke(protocols.clj:13)
889 at clojure.core$reduce.invokeStatic(core.clj:6824)
890 at clojure.core$reduce.invoke(core.clj:6810)
891 at clojure_noob.core$rand_part.invokeStatic(core.clj:39)
892 at clojure_noob.core$rand_part.invoke(core.clj:38)
893 at clojure_noob.core$_main.invokeStatic(core.clj:54)
894 at clojure_noob.core$_main.invoke(core.clj:48)
895 at clojure.lang.Var.invoke(Var.java:384)
896 at user$eval140.invokeStatic(form-init15519101999846500993.clj:1)
897 at user$eval140.invoke(form-init15519101999846500993.clj:1)
898 at clojure.lang.Compiler.eval(Compiler.java:7176)
899 at clojure.lang.Compiler.eval(Compiler.java:7166)
900 at clojure.lang.Compiler.load(Compiler.java:7635)
901 ... 12 more
And I don't have the slightest clue why that is. Also googling that first line of the error wont reveal useful information. Does anyone know the problem?
Upvotes: 3
Views: 144
Reputation: 45740
The issue is that you have a vector of mixed types being returned. Some of the elements are maps, some are lists. Note the first few entries of make-sym-parts
:
(make-sym-parts asym-hobbit-body-parts 3)
=>
[{:name "head", :size 3}
{:name "left-eye", :size 1}
({:name "0-eye", :size 1} {:name "1-eye", :size 1} {:name "2-eye", :size 1})
. . .
Look at the last entry that I listed here. It isn't a map; it's a list of maps. When you attempt to apply :size
to the list, you get nil
:
(:size '({:name "0-eye", :size 1} {:name "1-eye", :size 1} {:name "2-eye", :size 1}))
=> nil
And when you map :size
over the entire list, you get:
(->> (make-sym-parts asym-hobbit-body-parts 3)
(map :size))
=> (3 1 nil 1 nil 1 1 2 3 nil 3 nil 10 10 3 nil 6 1 nil 2 nil 2 nil 4 nil 3 nil 1 nil 2 nil)
This is causing a problem because you're giving those values to +
via reduce
, and +
will rightfully throw a fit if you give it a nil
since nil
isn't a number.
So, what's the fix? Honestly, I haven't written Clojure in like 3 months now, so I'm getting rusty, and I haven't read the problem statement, but it looks like you just need flatten that list out:
(defn make-sym-parts [asym-set num]
(reduce (fn [sink, {:keys [name size] :as body_part}]
(if (str/starts-with? name "left-")
(into sink (conj ; I threw in a call to conj here and rearranged it a bit
(for [i (range num)]
{:name (str/replace name #"^left" (str i))
:size size})
body_part))
(conj sink body_part)))
[]
asym-set))
(make-sym-parts asym-hobbit-body-parts 3)
=>
[{:name "head", :size 3}
{:name "left-eye", :size 1}
{:name "0-eye", :size 1}
{:name "1-eye", :size 1}
{:name "2-eye", :size 1}
{:name "left-ear", :size 1}
{:name "0-ear", :size 1}
{:name "1-ear", :size 1}
{:name "2-ear", :size 1}
{:name "mouth", :size 1}
{:name "nose", :size 1}
{:name "neck", :size 2}
{:name "left-shoulder", :size 3}
{:name "0-shoulder", :size 3}
{:name "1-shoulder", :size 3}
{:name "2-shoulder", :size 3}
{:name "left-upper-arm", :size 3}
{:name "0-upper-arm", :size 3}
{:name "1-upper-arm", :size 3}
{:name "2-upper-arm", :size 3}
{:name "chest", :size 10}
{:name "back", :size 10}
{:name "left-forearm", :size 3}
{:name "0-forearm", :size 3}
{:name "1-forearm", :size 3}
{:name "2-forearm", :size 3}
{:name "abdomen", :size 6}
{:name "left-kidney", :size 1}
{:name "0-kidney", :size 1}
{:name "1-kidney", :size 1}
{:name "2-kidney", :size 1}
{:name "left-hand", :size 2}
{:name "0-hand", :size 2}
{:name "1-hand", :size 2}
{:name "2-hand", :size 2}
{:name "left-knee", :size 2}
{:name "0-knee", :size 2}
{:name "1-knee", :size 2}
{:name "2-knee", :size 2}
{:name "left-thigh", :size 4}
{:name "0-thigh", :size 4}
{:name "1-thigh", :size 4}
{:name "2-thigh", :size 4}
{:name "left-lower-leg", :size 3}
{:name "0-lower-leg", :size 3}
{:name "1-lower-leg", :size 3}
{:name "2-lower-leg", :size 3}
{:name "left-achilles", :size 1}
{:name "0-achilles", :size 1}
{:name "1-achilles", :size 1}
{:name "2-achilles", :size 1}
{:name "left-foot", :size 2}
{:name "0-foot", :size 2}
{:name "1-foot", :size 2}
{:name "2-foot", :size 2}]
Inside the call to map
, you could also check if the element is a map or a list. If it's a list, you could call (map :size
again on the sublist. It depends if you want a flat list or a nested list as an end result. You also may be able to use mapcat
to get a flat list, although then you'd need to handle entries that aren't maps.
And how can you figure out the problem from that (very verbose) stack trace? Once you realize that bad deconstructions and bad key-lookups (like I described above) return nil
, it becomes much easier to reason about. Whenever you get an NPE, it's safe to assume right off the bat that you're deconstructing something wrong, or using the wrong key to lookup. These aren't the only reasons for a NPE, but in my experience in Clojure, they're the most common.
Read the stack trace top-to-bottom to trace where the bad data originated from and where is read used. Note my comments for tips on how to read it:
; If you have an NPE, that means you have a nil being passed somewhere...
Caused by: java.lang.NullPointerException
877 at clojure.lang.Numbers.ops(Numbers.java:1068)
878 at clojure.lang.Numbers.add(Numbers.java:153)
; ... so, you're passing a nil to + ("_PLUS_")
879 at clojure.core$_PLUS_.invokeStatic(core.clj:992)
880 at clojure.core$_PLUS_.invoke(core.clj:984)
881 at clojure.lang.ArrayChunk.reduce(ArrayChunk.java:63)
882 at clojure.core.protocols$fn__8139.invokeStatic(protocols.clj:136)
883 at clojure.core.protocols$fn__8139.invoke(protocols.clj:124)
884 at clojure.core.protocols$fn__8099$G__8094__8108.invoke(protocols.clj:19)
885 at clojure.core.protocols$seq_reduce.invokeStatic(protocols.clj:27)
886 at clojure.core.protocols$fn__8131.invokeStatic(protocols.clj:75)
887 at clojure.core.protocols$fn__8131.invoke(protocols.clj:75)
888 at clojure.core.protocols$fn__8073$G__8068__8086.invoke(protocols.clj:13)
; ... and it's happening inside a call to reduce
889 at clojure.core$reduce.invokeStatic(core.clj:6824)
; ... and that call to reduce is happening inside of rand-part
891 at clojure_noob.core$rand_part.invokeStatic(core.clj:39)
892 at clojure_noob.core$rand_part.invoke(core.clj:38)
893 at clojure_noob.core$_main.invokeStatic(core.clj:54)
You only have once such instance of +
being passed to reduce
inside of rand-part
, so that's a good place to start looking. From there, you just need to trace where the nil
is coming from using standard debugging techniques.
The take-away here is just scan over the stack trace to try to find words that you recognize. Unfortunately due to how Clojure names get "mangled" when translated into Java, names tend to be very verbose and noisy. You just need to kind of learn to "look through the noise" to find the relevant information. It gets easy after a little practice.
Some other things to note:
Don't use def inside of defn
. def
creates globals that aren't bound by scope. Use let
instead.
Try to use more indentation. A single space for indentation isn't great.
Clojure uses dash-case. You're using that in most parts, but body_part
seems to be a Python throwback.
If you want, you can post this code on Code Review and we can make suggestions to help you improve it.
Upvotes: 3