3 votes

J'ai deux versions d'une fonction pour compter les caractères de tête hash(#), laquelle est la meilleure ?

J'ai écrit un bout de code pour compter le caractère dièse (#) de tête d'une ligne, ce qui est un peu comme une ligne d'en-tête en Markdown

\### Line one          -> return 3
######## Line two     -> return 6 (Only care about the first 6 characters.

Version 1

(defn
  count-leading-hash
  [line]
  (let [cnt (count (take-while #(= % \#) line))]
    (if (> cnt 6) 6 cnt)))

Version 2

(defn
  count-leading-hash
  [line]
  (loop [cnt 0]
    (if (and (= (.charAt line cnt) \#) (< cnt 6))
      (recur (inc cnt))
      cnt)))

J'ai utilisé time pour mesurer les deux implémentations de tow, a constaté que la première version basée sur la technologie take-while est deux fois plus rapide que la version 2. Pris "###### Line one" en entrée, la version 1 a pris 0,09 msecs La version 2 a pris environ 0,19 msecs .

Question 1. S'agit-il recur qui ralentit la deuxième mise en œuvre ?

Question 2. La version 1 est plus proche du paradigme de la programmation fonctionnelle, n'est-ce pas ?

Question 3. Lequel préférez-vous ? Pourquoi ? (Nous vous invitons à rédiger votre propre mise en œuvre).

--Mise à jour--

Après avoir lu le doc de cloujure J'ai donc mis au point une nouvelle version de cette fonction, et je pense qu'elle est beaucoup plus claire.

(defn
  count-leading-hash
  [line]
  (->> line (take 6) (take-while #(= \# %)) count))

6voto

wvdlaan Points 61
  1. Il n'est pas utile de mesurer le temps pour de petits morceaux de code.
  2. Oui, la version 1 est plus fonctionnelle
  3. Je préfère la version 1 car il est plus facile de repérer les erreurs.
  4. Je préfère la version 1 parce qu'elle contient moins de code et que sa maintenance est donc moins coûteuse.

J'écrirais la fonction comme suit :

(defn count-leading-hash [line]
  (count (take-while #{\#} (take 6 line))))

3voto

Alex Taggart Points 5733
  1. Non, il s'agit de la réflexion utilisée pour invoquer .charAt . Appeler (set! *warn-on-reflection* true) avant de créer la fonction, et vous verrez l'avertissement.
  2. Dans la mesure où il utilise HOFs Bien sûr.
  3. La première, cependant (if (> cnt 6) 6 cnt) Il est préférable de l'écrire comme suit (min 6 cnt) .

2voto

Les micro-benchmarks sur la JVM sont presque toujours trompeurs, à moins que vous ne sachiez vraiment ce que vous faites. Je n'accorderais donc pas trop d'importance aux performances relatives de vos deux solutions.

La première solution est plus idiomatique. On ne voit vraiment de boucle explicite/récurrence dans le code Clojure que lorsqu'il s'agit de la seule alternative raisonnable. Dans ce cas, il est clair qu'il y a une alternative raisonnable.

Une autre option, si vous êtes à l'aise avec les expressions régulières :

(defn count-leading-hash [line]
     (count (or (re-find #"^#{1,6}" line) "")))

2voto

Gert Points 3131

1 : Non. recur est assez rapide. Pour chaque fonction que vous appelez, il y a un peu d'overhead et de "bruit" de la part de la VM : le REPL doit analyser et évaluer votre appel par exemple, ou un certain garbage collection peut se produire. C'est la raison pour laquelle les tests effectués sur de si petits morceaux de code ne signifient rien.

Comparer avec :

(defn
  count-leading-hash
  [line]
  (let [cnt (count (take-while #(= % \#) line))]
    (if (> cnt 6) 6 cnt)))

(defn
  count-leading-hash2
  [line]
  (loop [cnt 0]
    (if (and (= (.charAt line cnt) \#) (< cnt 6))
      (recur (inc cnt))
      cnt)))

(def lines ["### Line one" "######## Line two"])

(time (dorun (repeatedly 10000 #(dorun (map count-leading-hash lines)))))
;; "Elapsed time: 620.628 msecs"
;; => nil
(time (dorun (repeatedly 10000 #(dorun (map count-leading-hash2 lines)))))
;; "Elapsed time: 592.721 msecs"
;; => nil

Pas de différence significative.

2 : Utilisation loop / recur n'est pas idiomatique dans ce cas ; il est préférable de ne l'utiliser que lorsque vous en avez vraiment besoin et d'utiliser les autres fonctions disponibles lorsque vous le pouvez. Il existe de nombreuses fonctions utiles qui opèrent sur les collections/séquences. ClojureDocs à titre de référence et d'exemples. D'après mon expérience, les personnes qui ont des compétences en programmation impérative et qui découvrent la programmation fonctionnelle utilisent loop / recur beaucoup plus que ceux qui ont une grande expérience de Clojure ; loop / recur peut être une odeur de code.

3 : Je préfère la première version. Il y a beaucoup d'approches différentes :

;; more expensive, because it iterates n times, where n is the number of #'s
(defn count-leading-hash [line]
  (min 6 (count (take-while #(= \# %) line))))

;; takes only at most 6 characters from line, so less expensive
(defn count-leading-hash [line]
  (count (take-while #(= \# %) (take 6 line))))

;; instead of an anonymous function, you can use `partial`
(defn count-leading-hash [line]
  (count (take-while (partial = \#) (take 6 line))))

éditer : Comment décider du moment de l'utilisation partial vs une fonction anonyme ?

En termes de performances, cela n'a pas d'importance, car (partial = \#) s'évalue à (fn [& args] (apply = \# args)) . #(= \# %) se traduit par (fn [arg] (= \# arg)) . Les deux sont très similaires, mais partial vous donne une fonction qui accepte un nombre arbitraire d'arguments, donc dans les situations où vous en avez besoin, c'est la voie à suivre. partial est le (lambda) dans calcul lambda . Je dirais qu'il faut utiliser ce qui est le plus facile à lire, ou partial si vous avez besoin d'une fonction avec un nombre arbitraire d'arguments.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X