Я сейчас читаю "Clojure для смелых и верных" И в текущей главе они объясняют программу, которая берет вектор хэш-карт, представляющих части тела хоббита. Так как список с частями предоставляется только асимметрично (в него входят только левая рука, левый глаз и т.д.), то необходимо было написать функцию, которая добавляла бы соответствующие правые части. Позже было упражнение, чтобы расширить эту функцию, чтобы взять число и добавить это количество частей тела для каждой левой. Вторая функция случайным образом выбирала часть тела.
Это мой код:
(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)))))
Так
lein run 1
работает и распечатывает расширенный вектор
lein run 2
также работает и печатает случайное название части тела.
НО:
lein run 3
выдаст следующую ошибку:
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
И я не имею ни малейшего понятия, почему это так. Также поиск в Google этой первой строки ошибки не даст полезной информации. Кто-нибудь знает проблему?
Проблема в том, что у вас есть возвращаемый вектор смешанных типов. Некоторые элементы являются картами, некоторые — списками. Обратите внимание на первые несколько записей 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})
. . .
Посмотрите на последнюю запись, которую я перечислил здесь. Это не карта; это список карт. Когда вы пытаетесь применить :size
к списку, вы получаете nil
:
(:size '({:name "0-eye", :size 1} {:name "1-eye", :size 1} {:name "2-eye", :size 1}))
=> nil
И когда вы сопоставляете :size
весь список, вы получаете:
(->> (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)
Это вызывает проблему, потому что вы передаете эти значения +
через reduce
, и +
по праву вызовет истерику, если вы дадите ему nil
, поскольку nil
не является числом.
Итак, что исправить? Честно говоря, я не писал Clojure уже около 3 месяцев, поэтому я становлюсь ржавым, и я не читал условия задачи, но похоже, что вам просто нужно сгладить этот список:
(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}]
Внутри вызова map
вы также можете проверить, является ли элемент картой или списком. Если это список, вы можете снова вызвать (map :size
в подсписке. Это зависит от того, хотите ли вы плоский список или вложенный список в качестве конечного результата. Вы также можете использовать mapcat
для получения плоского списка, хотя тогда вам нужно будет обрабатывать записи, которые не являются картами.
И как вы можете понять проблему по этой (очень подробной) трассировке стека? Как только вы поймете, что плохие деконструкции и поиск ключей (как я описал выше) возвращают nil
, становится намного легче рассуждать. Всякий раз, когда вы получаете NPE, можно сразу предположить, что вы разбираете что-то неправильно или используете неправильный ключ для поиска. Это не единственные причины NPE, но, судя по моему опыту работы с Clojure, они наиболее распространены.
Прочтите трассировку стека сверху вниз, чтобы отследить, откуда были получены неверные данные и где они используются для чтения. Обратите внимание на мои комментарии для советов о том, как его читать:
; 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)
У вас есть только один такой случай, когда +
передается reduce
внутри rand-part
, так что это хорошее место для начала поиска. Оттуда вам просто нужно отследить, откуда берется nil
, используя стандартные методы отладки.
Вывод здесь — просто просканируйте трассировку стека, чтобы попытаться найти слова, которые вы узнаете. К сожалению, из-за того, что имена Clojure «искажаются» при переводе на Java, имена имеют тенденцию быть очень многословными и зашумленными. Вам просто нужно как бы научиться «смотреть сквозь шум», чтобы находить нужную информацию. Это становится легко после небольшой практики.
Некоторые другие вещи, чтобы отметить:
Не используйте def внутри defn
. def
создает глобальные переменные, не связанные областью видимости. Вместо этого используйте let
.
Попробуйте использовать больше отступов. Одно пространство для отступа — это не очень хорошо.
Clojure использует регистр тире. Вы используете это в большинстве частей, но body_part
кажется возвратом к Python.
Если вы хотите, вы можете опубликовать этот код на Code Review, и мы можем внести предложения, которые помогут вам улучшить его.
@Узаку Нет проблем. Рад помочь. Я посмотрю, смогу ли я написать отзыв позже. Если не я, то я уверен, что кто-то еще прыгнет на это. У нас не так много Clojure для обзора.
Я думаю, что это был самый полезный ответ, который я когда-либо получал на Stackoverflow, спасибо. А также за информацию о том, как улучшить код. Я обязательно воспользуюсь codereview, о котором еще не знал, так что и за это спасибо :D