¿Buenos ejemplos del uso de macros de Clojure que demuestran las ventajas del lenguaje sobre la corriente principal?

Estoy pensando en aprender Clojure, pero viniendo del mundo basado en la sintaxis c (java, php, c#) de los lenguajes imperativos va a ser un desafío, por lo que uno se pregunta, ¿realmente vale la pena? Y aunque tal declaración de preguntas puede ser muy subjetiva y difícil de manejar, hay un rasgo específico de Clojure (y más generalmente, los ceceos) sobre el que sigo leyendo, que se supone que lo convierte en el lenguaje más flexible: las macros.

¿Tiene algún buen ejemplo del uso de macros en Clojure, para propósitos que en otros lenguajes convencionales (considere cualquiera de C++, PHP, Perl, Python, Groovy/Java, C#, JavaScript) requerirían soluciones mucho menos elegantes/muchas funciones innecesarias? abstracción/hacks/etc.

preguntado el 03 de mayo de 12 a las 16:05

Eche un vistazo aquí a algunas macros geniales: blog.klipse.tech/clojure/2016/05/09/macro-tutorial-4.html -

6 Respuestas

Las macros me parecen bastante útiles para definir nuevas características del lenguaje. En la mayoría de los idiomas, debe esperar una nueva versión del idioma para obtener una nueva sintaxis; en Lisp, puede simplemente ampliar el lenguaje central con macros y agregar las funciones usted mismo.

Por ejemplo, Clojure no tiene un estilo C imperativo for(i=0 ;i<10; i++) bucle, pero puede agregar fácilmente uno con una macro:

(defmacro for-loop [[sym init check change :as params] & steps]
  (cond
    (not (vector? params)) 
      (throw (Error. "Binding form must be a vector for for-loop"))
    (not= 4 (count params)) 
      (throw (Error. "Binding form must have exactly 4 arguments in for-loop"))
    :default
      `(loop [~sym ~init value# nil]
         (if ~check
           (let [new-value# (do ~@steps)]
             (recur ~change new-value#))
           value#))))

Uso de la siguiente manera:

(for-loop [i 0, (< i 10), (inc i)]
  (println i))

Si es una buena idea agregar un bucle imperativo a un lenguaje funcional es un debate que probablemente deberíamos evitar aquí :-)

contestado el 03 de mayo de 12 a las 17:05

Genial, este es el tipo de cosas que estaba buscando. (si (tiene-más usted) (por favor-continúe)) ;) - Cray

Mi biblioteca clojure-utils en GitHub tiene una colección en constante crecimiento de macros y funciones similares: github.com/mikera/clojure-utils - mikera

hay muchas macros en la base de clojure en las que no piensas... Lo cual es señal de una buena macro, te dejan extender el idioma de manera que haga la vida más fácil. Sin macros, la vida sería mucho menos emocionante. por ejemplo, si no tuviéramos

(with-out-str (somebody else's code that prints to screen))

entonces necesitaría modificar su código de formas a las que quizás no tenga acceso.

otro gran ejemplo es

(with-open-file [fh (open-a-file-code ...)]
   (do (stuff assured that the file won't leak)))

el conjunto with-something-do El patrón de macros realmente se ha sumado al sistema ecológico de Clojure.


la otra cara de la proverbial moneda macro es que paso esencialmente todo mi tiempo profesional (actual) de Clojure usando una biblioteca muy pesada de macros y, por lo tanto, paso mucho tiempo solucionando el hecho de que las macros no se componen bien y no son de primera clase. Los autores de esta biblioteca harán todo lo posible en la próxima versión para que toda la funcionalidad esté disponible sin pasar por las macros para permitir que personas como yo las usen en funciones de orden superior como map y reduce.

las macros mejoran el mundo cuando facilitan la vida. Pueden tener el efecto contrario cuando son la única interfaz de una biblioteca. por favor, no use macros como interfaces

En general, es difícil conseguir que la forma de los datos sea verdaderamente correcta. Si como autor de una biblioteca, tiene los datos bien estructurados sobre cómo imagina que se usará su biblioteca, es muy posible que haya una manera de reestructurar las cosas para permitir que los usuarios utilicen su biblioteca de formas nuevas e inimaginables. En este caso, la estructura de la biblioteca fantástica en cuestión era bastante buena, permitía cosas que los autores no habían previsto. Desafortunadamente, una gran biblioteca estaba restringida porque su interfaz era un conjunto de macros, no un conjunto de funciones. la biblioteca era mejor que sus macros, así que la retuvieron. Esto no quiere decir que las macros tengan la culpa de ninguna manera, solo que la programación es difícil y son otra herramienta que puede tener muchos efectos y todas las piezas deben usarse juntas para que funcionen bien.

contestado el 03 de mayo de 12 a las 18:05

¡Maldita sea! Estaba escribiendo una respuesta a lo largo de estas líneas, así que solo haré +1 en esta :) PD Creo que en clojure la macro es with-open. - ivan

with-out-str es realmente ordenado. ¡Gracias por eso! - el dia gira

¿Podrías explicar tu segundo ejemplo? ¿A qué te refieres con fuga? Sobre el uso de macros como interfaces, en su opinión, ¿cree que fue una muy mala decisión por parte de los autores de la biblioteca, o las macros de clojure simplemente funcionan de tal manera que es demasiado fácil usarlas en exceso para cualquiera que no sea un desarrollador muy hábil? - Cray

Aunque with-out-str realmente es una gran conveniencia, no es el aspecto macro aquí lo que es clave. la clave son enlaces dinámicos y el hecho de que *out* es una var ligada dinámicamente. Macro simplemente hace que sea más fácil delimitar la parte del código donde *out* es rebote en un StringWriter. - Marko Topolnik

También hay un caso de uso más esotérico para macros que a veces uso: escribir código conciso y legible que también está completamente optimizado. He aquí un ejemplo trivial:

(defmacro str* [& ss] (apply str (map eval ss)))

Lo que esto hace es concatenar cadenas en tiempo de compilación (por supuesto, tienen que ser constantes en tiempo de compilación). La función regular de concatenación de cadenas en Clojure es str por lo tanto, siempre que en un código de bucle cerrado tenga una cadena larga que me gustaría dividir en varios literales de cadena, solo agrego la estrella a str y cambie la concatenación de tiempo de ejecución a tiempo de compilación. Uso:

(str* "I want to write some very lenghty string, most often it will be a complex"
      " SQL query. I'd hate if it meant allocating the string all over every time"
      " this is executed.")

Otro ejemplo menos trivial:

(defmacro jprint [& xs] `(doto *out* ~@(for [x xs] `(.append ~x))))

La & significa que acepta un número variable de argumentos (varargs, función variádica). En Clojure, una llamada de función variádica hace uso de una colección asignada en montón para transferir los argumentos (como en Java, que usa una matriz). Esto no es muy óptimo, pero si uso una macro como la anterior, entonces no hay llamada de función. Yo lo uso así:

(jprint \" (json-escape item) \")

Se compila en tres advocaciones de PrintWriter.append (básicamente un bucle desenrollado).

Finalmente, me gustaría mostrarles algo aún más radicalmente diferente. Puede usar una macro para ayudarlo a definir una clase de funciones similares, eliminando vasto cantidades de repetitivo. Tome este ejemplo familiar: en una biblioteca de cliente HTTP queremos una función separada para cada uno de los métodos HTTP. Cada definición de función es bastante compleja ya que tiene cuatro firmas sobrecargadas. Además, cada función involucra una clase de solicitud diferente de la biblioteca Apache HttpClient, pero todo lo demás es exactamente igual para todos los métodos HTTP. Mira cuánto código necesito para manejar esto.

(defmacro- def-http-method [name]
  `(defn ~name
     ([~'url ~'headers ~'opts ~'body]
        (handle (~(symbol (str "Http" (s/capitalize name) ".")) ~'url) ~'headers ~'opts ~'body))
     ([~'url ~'headers ~'opts] (~name ~'url ~'headers ~'opts nil))
     ([~'url ~'headers] (~name ~'url ~'headers nil nil))
     ([~'url] (~name ~'url nil nil nil))))

(doseq [m ['GET 'POST 'PUT 'DELETE 'OPTIONS 'HEAD]]
  (eval `(def-http-method ~m)))

contestado el 05 de mayo de 12 a las 11:05

+1 para algunos buenos usos de macro. ¡Puedo robar algunas de estas ideas, especialmente como el truco de desenrollar el bucle! - mikera

Dato curioso: hay Lisps de propósito especial construidos alrededor de esta idea. GOAL/GOOL son Lisps completamente compilados que generan código ensamblador optimizado. Fueron las principales herramientas en la producción de algunos juegos de consola. - Rafael Dowgird

Algo de código real de un proyecto en el que estaba trabajando: quería bucles for-esque anidados usando ints sin caja.

(defmacro dofor
  [[i begin end step & rest] & body]
  (when step
    `(let [end# (long ~end)
           step# (long ~step)
           comp# (if (< step# 0)
                   >
                   <)]
       (loop [~i ~begin]
         (when (comp# ~i end#)
           ~@(if rest
               `((dofor ~rest ~@body))
               body)
           (recur (unchecked-add ~i step#)))))))

Usado como

(dofor [i 2 6 2
        j i 6 1]
  (println i j))

Que imprime

2 2
2 3
2 4
2 5
4 4
4 5

Se compila a algo muy cercano a la materia prima loop/recurs que originalmente estaba escribiendo a mano, por lo que básicamente no hay una penalización en el rendimiento del tiempo de ejecución, a diferencia del equivalente

(doseq [i (range 2 6 2)
        j (range i 6 1)]
  (println i j))

Creo que el código resultante se compara bastante favorablemente con el equivalente de Java:

for (int i = 2; i < 6; i+=2) {
    for (int j = i; j < 6; j++) {
        System.out.println(i+" "+j);
    }
}

contestado el 09 de mayo de 12 a las 02:05

Un ejemplo simple de una macro útil que es bastante difícil de recrear sin macros es doto. Evalúa su primer argumento y luego evalúa las siguientes formas, insertando el resultado de la evaluación como su primer argumento. Esto puede no parecer mucho, pero...

Con doto modo:

(let [tmpObject (produceObject)]
 (do
   (.setBackground tmpObject GREEN)
   (.setThis tmpObject foo)
   (.setThat tmpObject bar)
   (.outputTo tmpObject objectSink)))

Se convierte en eso:

(doto (produceObject)
   (.setBackground GREEN)
   (.setThis foo)
   (.setThat bar)
   (.outputTo objectSink))

Lo importante es que doto no es mágico: puede (re)construirlo usted mismo utilizando las características estándar del lenguaje.

contestado el 05 de mayo de 12 a las 18:05

Entonces, ¿esto es básicamente como una declaración "con" en JS y un par de otros idiomas? - Cray

@Cray Sí, es un "con" que puede implementar usted mismo sin sumergirse en las fuentes del compilador para cambiar la sintaxis que reconoce. - Rafael Dowgird

Tenga en cuenta también que doto funciona con cualquier cosa que se ajuste a la misma sintaxis: la invocación de métodos es solo un caso de uso. - Marko Topolnik

@Marko Cierto, doto funciona igualmente bien con funciones sencillas y con otras macros, aunque probablemente se use más a menudo para establecer muchas propiedades en un objeto Java. - Rafael Dowgird

Solo pensé que era útil para OP notar esa generalidad. - Marko Topolnik

Las macros son parte de Clojure, pero en mi humilde opinión no creo que sean la razón por la que deberías o no deberías aprender Clojure. La inmutabilidad de los datos, las buenas construcciones para manejar el estado concurrente y el hecho de que es un lenguaje JVM y puede aprovechar el código Java son tres razones. Si no puede encontrar otra razón para aprender Clojure, considere el hecho de que un lenguaje de programación funcional probablemente debería afectar positivamente la forma en que aborda los problemas en cualquier lenguaje.

Para ver las macros, le sugiero que comience con las macros de subprocesamiento de Clojure: subproceso primero y subproceso último -> y ->> respectivamente; visita esta páginay muchos de los varios blogs que hablan de Clojure .

Buena suerte y diviertete.

contestado el 03 de mayo de 12 a las 16:05

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.