Hackerrank: бить рекорды
Я изучаю Clojure и имею в нем ранг n00b, пытаюсь учиться по книгам и онлайн-руководствам (но иногда меня беспокоит, что я приобретаю плохие привычки или, по крайней мере, не все хорошие). В качестве упражнения я выполнил задачу «Бить рекорды» на Hackerrank.
TL; Описание проблемы DR:
Для списка оценок (в историческом порядке) подсчитайте, сколько раз предыдущий лучший результат был превышен, а предыдущий худший результат был занижен.
В итеративном языке довольно легко просто перебирать список ; но Clojure не выполняет итераций, и я решил заняться этой проблемой в качестве упражнения при построении рекурсивного решения (хвостовой части). Чтобы упростить себе задачу, я сначала разработал рекурсивное решение на Java, которое затем перевел. В общем, это довольно простая рекурсивная функция без ракетной хирургии.
Очевидно, мой код работает, как показывают включенные модульные тесты. Однако меня беспокоит следующее:
- Если положить рядом с кодом Java, они выглядят довольно похоже. Следил ли я за «идиоматическим» программированием на Clojure, или это просто неуклюжая «дословная транслитерация»?
- Есть ли области, которые можно было бы сделать более компактными и / или более простыми для понимания при использовании различных конструкций Clojure?
Мы будем очень признательны за ваш критический вклад, в том числе в отношении модульного тестирования , поскольку это, возможно, важная часть программирования, которую я хочу изучать и практиковать параллельно.
Код:
(ns hackerrank.breaking-records
(:require [clojure.test :refer :all]))
(defrecord Record [min max countworse countbetter])
(defn recalc-record [rec newscore]
(Record.
(min newscore (:min rec))
(max newscore (:max rec))
(+ (:countworse rec) (if (> (:min rec) newscore) 1 0))
(+ (:countbetter rec) (if (< (:max rec) newscore) 1 0))))
(defn accumulate [curr-record remaining-scores]
(if (nil? (second remaining-scores))
curr-record
(recur (recalc-record curr-record (second remaining-scores)) (rest remaining-scores)))
)
(defn breaking-records [scores]
(let [result (accumulate (Record. (first scores) (first scores) 0 0) scores)]
(list (:countbetter result) (:countworse result))))
(deftest test-records
(testing "edge cases"
(is (= '(0 0) (breaking-records '())) "no games played yet")
(is (= '(0 0) (breaking-records '(5))) "single game"))
(testing "hackerrank examples"
(is (= '(2 4) (breaking-records '(10 5 20 20 4 5 2 25 1))))
(is (= '(4 0) (breaking-records '(3 4 21 36 10 28 35 5 24 42)))))
)
Ответы
Я переписал ваше решение, чтобы использовать более типичные функции Clojure. Когда вы перебираете данные и вам нужно отслеживать накопленное состояние, это трудно превзойти loop/recur. Первый пример:
(ns tst.demo.core
(:use clojure.test))
(defn breaking-records
[scores]
; this loop has 5 variables. Init all of them
(loop [low (first scores)
high (first scores)
nworse 0
nbetter 0
score-pairs (partition 2 1 scores)]
(if (empty? score-pairs)
{:nbetter nbetter :nworse nworse}
(let [curr-score-pair (first score-pairs)
new-score (second curr-score-pair)]
; start the next iteration with modified versions of the 5 loop vars
(recur
(min new-score low)
(max new-score high)
(if (< new-score low)
(inc nworse)
nworse)
(if (< high new-score)
(inc nbetter)
nbetter)
(rest score-pairs))))))
и модульные тесты:
(deftest test-records
(testing "edge cases"
(is (= (breaking-records []) {:nbetter 0 :nworse 0}) "no games played yet")
(is (= (breaking-records [5]) {:nbetter 0 :nworse 0}) "single game"))
(testing "hackerrank examples"
(is (= (breaking-records [10 5 20 20 4 5 2 25 1]) {:nbetter 2 :nworse 4}))
(is (= (breaking-records [3 4 21 36 10 28 35 5 24 42]) {:nbetter 4 :nworse 0}))))
; ***** NOTE: it's much easier to use vectors like [1 2 3] instead of a quoted list `(1 2 3)
См. Этот список документации , особенно. Шпаргалка по Clojure. Кроме того, проект шаблона в целом показывает, как мне нравится структурировать вещи. :)
Функция, которая помогает больше всего, - это partition. См. Документы .
Небольшой рефакторинг
Вы можете немного упростить его и сделать немного более компактным, используя более специализированные функции, такие как reduceи cond->. Эта версия использует карту для хранения состояния и reduceвыполнения цикла:
(defn breaking-records
[scores]
(let [state-init {:low (first scores)
:high (first scores)
:nworse 0
:nbetter 0}
accum-stats-fn (fn [state score-pair]
; Use map destructuring to pull out the 4 state variables
(let [{:keys [low high nworse nbetter]} state
new-score (second score-pair)
state-new {:low (min new-score low)
:high (max new-score high)
:nworse (cond-> nworse
(< new-score low) (inc))
:nbetter (cond-> nbetter
(< high new-score) (inc))}]
state-new))
state-final (reduce accum-stats-fn
state-init
(partition 2 1 scores))
result (select-keys state-final [:nworse :nbetter])]
result))