Очень простая учебная программа не компилируется

Я сейчас читаю "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 этой первой строки ошибки не даст полезной информации. Кто-нибудь знает проблему?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
116
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема в том, что у вас есть возвращаемый вектор смешанных типов. Некоторые элементы являются картами, некоторые — списками. Обратите внимание на первые несколько записей 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, и мы можем внести предложения, которые помогут вам улучшить его.

Я думаю, что это был самый полезный ответ, который я когда-либо получал на Stackoverflow, спасибо. А также за информацию о том, как улучшить код. Я обязательно воспользуюсь codereview, о котором еще не знал, так что и за это спасибо :D

Uzaku 30.07.2019 12:40

@Узаку Нет проблем. Рад помочь. Я посмотрю, смогу ли я написать отзыв позже. Если не я, то я уверен, что кто-то еще прыгнет на это. У нас не так много Clojure для обзора.

Carcigenicate 30.07.2019 15:55

Другие вопросы по теме