Hackerrank: quebrando recordes
Estou aprendendo Clojure e sou um n00b graduado nisso, tentando aprender com livros e tutoriais online (mas às vezes fico preocupado por estar adquirindo maus hábitos ou pelo menos não todos os bons hábitos). Para o exercício, fiz o problema Quebrando os Recordes no Hackerrank.
TL; descrição do problema de DR:
Para obter uma lista de pontuações (em ordem histórica), conte o número de vezes que a melhor pontuação anterior foi excedida e a pior pontuação anterior foi reduzida.
Em uma linguagem iterativa, é muito fácil apenas iterar pela lista ; mas Clojure não faz iteração e decidi abordar esse problema como exercício de construção de uma solução recursiva (extremidade final). Para tornar mais fácil para mim, primeiro fiz uma solução recursiva em Java, que depois traduzi. Em suma, é uma função recursiva bastante simples, sem cirurgia de foguete envolvida.
Obviamente, meu código funciona, conforme mostrado pelos testes de unidade incluídos. No entanto, minhas preocupações são as seguintes:
- Quando colocados ao lado do código Java, os dois parecem bastante semelhantes. Segui a programação "idiomática" do Clojure ou é apenas uma "transliteração palavra por palavra" desajeitada?
- Existem áreas que poderiam ter sido mais compactas e / ou mais fáceis de entender usando diferentes construções Clojure?
Sua contribuição crítica será muito apreciada - inclusive com relação ao teste de unidade , já que é indiscutivelmente uma parte importante da programação, que desejo aprender e praticar em paralelo.
Código:
(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)))))
)
Respostas
Reescrevi sua solução para usar recursos mais típicos do Clojure. Quando você está fazendo um loop de dados e precisa controlar o estado acumulado, é difícil superar loop/recur. Um primeiro exemplo:
(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))))))
e testes de unidade:
(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)
Por favor, consulte esta lista de documentação , esp. o Clojure CheatSheet. Além disso, o projeto-modelo como um todo mostra como gosto de estruturar as coisas. :)
A função que mais ajuda é partition. Veja a documentação .
Ligeira refatoração
Você pode simplificá-lo um pouco e torná-lo um pouco mais compacto usando funções mais especializadas como reducee cond->. Esta versão usa um mapa para manter o estado e reducerealizar o loop:
(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))