Search code examples
clojure

Very simple practice program won't compile


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?


Solution

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