Hackerrank: battre les records
J'apprends Clojure et j'y suis classé n00b, j'essaie d'apprendre des livres et des tutoriels en ligne (mais je suis parfois préoccupé par le fait que je prends de mauvaises habitudes ou du moins pas toutes les bonnes habitudes). Pour l'exercice, j'ai fait le problème Breaking the Records sur Hackerrank.
TL; Description du problème DR:
Pour une liste de scores (dans l'ordre historique), comptez le nombre de fois où le meilleur score précédent a été dépassé et le pire score précédent a été sous-estimé.
Dans un langage itératif, il est assez facile de simplement parcourir la liste ; mais Clojure ne fait pas d'itération et j'ai décidé de m'attaquer à ce problème pour m'exercer à construire une solution récursive (à l'extrémité arrière). Pour me faciliter la tâche, j'ai d'abord fait une solution récursive en Java, que j'ai ensuite traduite. Dans l'ensemble, c'est une fonction récursive assez simple sans chirurgie de fusée impliquée.
Évidemment, mon code fonctionne, comme le montrent les tests unitaires inclus. Mes préoccupations sont cependant les suivantes:
- Lorsqu'ils sont placés à côté du code Java, les deux semblent assez similaires. Ai-je suivi la programmation Clojure "idiomatique", ou est-ce juste une "translittération mot à mot" maladroite?
- Y a-t-il des domaines qui auraient pu être plus compacts et / ou plus faciles à comprendre en utilisant différentes constructions Clojure?
Votre contribution critique sera très appréciée - y compris en ce qui concerne les tests unitaires , car c'est sans doute une partie importante de la programmation, que je veux apprendre et pratiquer en parallèle.
Code:
(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)))))
)
Réponses
J'ai réécrit votre solution pour utiliser des fonctionnalités Clojure plus typiques. Lorsque vous bouclez sur des données et que vous avez besoin de garder une trace de l'état accumulé, il est difficile de le battre loop/recur. Un premier exemple:
(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))))))
et tests unitaires:
(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)
Veuillez consulter cette liste de documentation , esp. le Clojure CheatSheet. En outre, le projet de modèle dans son ensemble montre comment j'aime structurer les choses. :)
La fonction qui aide le plus est partition. Consultez la documentation .
Léger refactoring
Vous pouvez le simplifier un peu et le rendre un peu plus compact en utilisant des fonctions plus spécialisées comme reduceet cond->. Cette version utilise une carte pour maintenir l'état et reduceeffectuer le bouclage:
(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))