Hackerrank: batiendo récords

Oct 22 2020

Estoy aprendiendo Clojure y soy un rango n00b en eso, tratando de aprender de libros y tutoriales en línea (pero a veces me preocupa que esté adquiriendo malos hábitos o al menos no todos los buenos hábitos). Para el ejercicio hice el problema Breaking the Records en Hackerrank.

TL; Descripción del problema DR:

Para obtener una lista de puntuaciones (en orden histórico), cuente el número de veces que se superó la mejor puntuación anterior, así como también se redujo la peor puntuación anterior.

En un lenguaje iterativo, es bastante fácil iterar por la lista ; pero Clojure no hace iteración y decidí abordar este problema para ejercitar la construcción de una solución recursiva (final). Para facilitarme las cosas, primero hice una solución recursiva en Java, que luego traduje. Con todo, es una función recursiva bastante simple sin la cirugía de cohetes involucrada.

Obviamente, mi código funciona, como lo muestran las pruebas unitarias incluidas. Sin embargo, mis preocupaciones son las siguientes:

  • Cuando se colocan junto al código Java, los dos se ven bastante similares. ¿Seguí la programación "idiomática" de Clojure, o es simplemente una "transliteración palabra por palabra" torpe?
  • ¿Hay áreas que podrían haber sido más compactas y / o más fáciles de entender al usar diferentes construcciones de Clojure?

Su contribución crítica será muy apreciada, incluso con respecto a las pruebas unitarias , ya que podría decirse que es una parte importante de la programación, que quiero aprender y practicar en 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)))))
)

Respuestas

2 AlanThompson Oct 23 2020 at 02:14

Reescribí su solución para usar funciones más típicas de Clojure. Cuando está recorriendo datos y necesita realizar un seguimiento del estado acumulado, es difícil de superar loop/recur. Un primer ejemplo:

(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))))))

y pruebas unitarias:

(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)

Consulte esta lista de documentación , esp. la CheatSheet de Clojure. Además, el proyecto de plantilla en su conjunto muestra cómo me gusta estructurar las cosas. :)

La función que más ayuda es partition. Consulte los documentos .


Ligera refactorización

Puede simplificarlo un poco y hacerlo un poco más compacto utilizando funciones más especializadas como reducey cond->. Esta versión usa un mapa para mantener el estado y reducerealizar el bucle:

(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))