¿Cómo escribir un código Clojure legible?

13

Soy nuevo en Clojure. Puedo entender el código que escribo pero se vuelve demasiado difícil de entender más tarde.
Se hace difícil hacer coincidir los paréntesis.

¿Cuáles son las convenciones genéricas a seguir con respecto a las convenciones de nomenclatura y la sangría en diversas situaciones?

Por ejemplo, escribí un ejemplo de desestructuración de muestra para entender, pero parece completamente ilegible la segunda vez.

(defn f [{x :x y :y z :z [a b c] :coll}] (print x " " y  " " z " " a " " b " " c)) 

En caso de desestructuración, ¿es mejor hacerlo directamente a nivel de parámetro o iniciar un formulario let y luego continuar allí?

Amogh Talpallikar
fuente
3
Hay una buena respuesta sobre la legibilidad en Stack Overflow. Puedes echarle un vistazo. stackoverflow.com/a/1894891/1969106
yfklon
2
Escribir código legible de Lisp es difícil en general. Inventaron el backronym "Perdido en paréntesis superfluos" por una razón.
Mason Wheeler

Respuestas:

23

Convenciones de nombres

  • permanecer en minúsculas para las funciones
  • use -para la separación silábica (lo que sería un guión bajo o un caso de camello en otros idiomas).

    (defn add-one [i] (inc i))

  • Los predicados (es decir, las funciones que devuelven verdadero o falso) terminan con ? Ejemplos:odd? even? nil? empty?

  • Los procedimientos de cambio de estado terminan en !. Te acuerdas set!verdad? oswap!

  • Elija longitudes cortas de nombres variables según su alcance. Eso significa que si tiene una variable auxiliar realmente pequeña, a menudo puede usar un nombre de una letra. (map (fn [[k v]] (inc v)) {:test 4 :blub 5})elija nombres de variables más largos según sea necesario, especialmente si se usan para muchas líneas de código y no puede adivinar de inmediato su propósito. (mi opinión).

    Siento que muchos programadores de clojure tienden a usar nombres genéricos y cortos. Pero, por supuesto, esto no es realmente una observación objetiva. El punto es que muchas funciones de clojure son en realidad bastante genéricas.

Funciones lambda

  • En realidad, puede nombrar funciones lambda. Esto es conveniente para depurar y perfilar (mi experiencia aquí es con ClojureScript).

    (fn square-em [[k v]] {k (* v v)})

  • Utilice las funciones lambda en línea #()como sea conveniente

Espacio en blanco

  • No debe haber líneas solo para padres. Es decir, cerrar los paréntesis de inmediato. Recuerde que los parens están ahí para el editor y el compilador, la sangría es para usted.

  • Las listas de parámetros de funciones van a una nueva línea

   (defn contras
     [ab]
     (lista ab))

Esto tiene sentido si piensas en las cadenas de documentos. Están entre el nombre de la función y los parámetros. La siguiente cadena de documentos probablemente no sea la más sabia;)

   (defn contras
     "Emparejar cosas"
     [ab]
     (lista ab))
  • Los datos emparejados se pueden separar por una nueva línea siempre que conserve el emparejamiento
  (defn f 
    [{x: x 
      y: y 
      z: z  
      [abc]: coll}] 
    (imprimir x "" y "" z "" a "" b "" c)) 

(También puedes ingresar ,como quieras pero esto se siente incómodo).

  • Para la sangría, use un editor suficientemente bueno. Hace años, esto era emacs para la edición lisp, vim también es genial hoy. Los IDE de clojure típicos también deberían proporcionar esta funcionalidad. Simplemente no use un editor de texto aleatorio.

    En vim en modo comando, puede usar el =comando para sangrar correctamente.

  • Si el comando se alarga demasiado (anidado, etc.), puede insertar una nueva línea después del primer argumento. Ahora el siguiente código no tiene mucho sentido, pero ilustra cómo puedes agrupar y sangrar expresiones:

(+ (if-let [age (: personal-age coll)]
     (si (> 18 años)
       años
       0))
   (cuenta (rango (- 3 b)
                 (reducir + 
                         (rango b 10)))))

Una buena sangría significa que no tiene que contar los corchetes. Los corchetes son para la computadora (para interpretar el código fuente y sangrarlo). La sangría es para su fácil comprensión.

Funciones de orden superior vs. fory doseqformas

Viniendo de un fondo de Scheme, estaba bastante orgulloso de haber entendido mapy funciones lambda, etc. Muy a menudo, escribía algo como esto

(map (fn [[k x]] (+ x (k data))) {:a 10 :b 20 :c 30})

Esto es bastante difícil de leer. La forforma es mucho mejor:

(for [[k x] {:a 10 :b 20 :c30}]
  (+ x (k data)))

`map tiene muchos usos y es realmente agradable si está utilizando funciones con nombre. Es decir

(map inc [12 30 10]

(map count [[10 20 23] [1 2 3 4 5] (range 5)])

Usar macros de subprocesos

Utilice las macros de subprocesos ->y ->>también dotocuando corresponda.

El punto es que las macros de subprocesos hacen que el código fuente parezca más lineal que la composición de funciones. El siguiente fragmento de código es bastante ilegible sin la macro de subprocesos:

   (f (g (h 3) 10) [10 3 2 3])

Comparar con

   (-> 
     (h 3)
     (g 10)
     (f [10 3 2 3]))

Al usar la macro de subprocesos, normalmente se puede evitar la introducción de variables temporales que solo se usan una vez.

Otras cosas

  • Use docstrings
  • mantener funciones cortas
  • leer otro código de clojure
wirrbel
fuente
¡Esa función con desestructuración se ve hermosa con sangría!
Amogh Talpallikar
+1 para mantener las funciones cortas. Muchas funciones pequeñas son mucho más auto documentadas
Daniel Gratzer
1
Estoy totalmente en desacuerdo con que es una buena idea usar nombres de variables cortos, incluso en funciones de "corto alcance". Los buenos nombres de variables son críticos para la legibilidad y no cuestan nada excepto las pulsaciones de teclas. Esta es una de las cosas que más me molesta de la comunidad de Clojure. Hay muchas personas con una resistencia casi hostil a los nombres de variables descriptivas. Clojure core está lleno de nombres de variables de 1 letra para argumentos de función, y eso hace que sea mucho más difícil aprender el idioma (por ejemplo, ejecutar doco sourceen un REPL). Fin de la diatriba, por una excelente respuesta
Nathan Wallace
@NathanWallace En cierto modo estoy de acuerdo contigo, pero en algunos aspectos no. Los nombres largos a veces tienden a hacer que las funciones sean demasiado específicas. Por lo tanto, es posible que alguna operación de filtro general sea de hecho general, mientras que cuando el argumento era en appleslugar de xs, pensabas que era específico de las manzanas. Entonces, también consideraría que los nombres de argumentos de funciones son más amplios que digamos una variable de bucle for. así que si es necesario, puede tenerlos por más tiempo. Como último pensamiento: te dejo con "Código de nombre, no valores" concatenative.org/wiki/view/Concatenative%20language/…
wirrbel
Podría agregar un párrafo sobre algo como las interfaces privadas vs públicas. Especialmente con respecto a las bibliotecas. Es un aspecto de la calidad del código del que no se habla lo suficiente y he aprendido mucho sobre esto desde que escribí esa respuesta.
wirrbel