types clojure discriminated-union

types - Manera idiomática de representar el tipo de suma(O ab) en Clojure



discriminated-union (7)

¿Cómo representan los tipos de suma, también conocidos como uniones etiquetadas y registros de variantes? Algo como Either ab en Haskell o Either[+A, +B] en Scala.

Either tiene dos usos: para devolver un valor de uno de dos tipos o para devolver dos valores del mismo tipo que deberían tener una semántica diferente según la etiqueta.

El primer uso solo es importante cuando se usa un sistema de tipo estático. Either es básicamente la solución mínima posible, dadas las restricciones del sistema de tipos Haskell. Con un sistema de tipo dinámico, puede devolver valores de cualquier tipo que desee. Either es necesario.

El segundo uso es importante, pero se puede lograr de manera muy simple de dos (o más) maneras:

  1. {:tag :left :value 123} {:tag :right :value "hello"}
  2. {:left 123} {:right "hello"}

Lo que me gustaría asegurar es que: la etiqueta siempre está ahí, y puede tomar solo uno de los valores especificados, y el valor correspondiente es consistentemente del mismo tipo / comportamiento y no puede ser nulo, y hay una manera fácil de Ver que me ocupé de todos los casos en el código.

Si desea garantizar esto de forma estática, Clojure probablemente no sea su idioma. El motivo es simple: las expresiones no tienen tipos hasta el tiempo de ejecución, hasta que devuelven un valor.

La razón por la que una macro no funcionará es que, en el momento de la expansión de la macro, no tiene valores de tiempo de ejecución y, por lo tanto, tipos de tiempo de ejecución. Tiene construcciones en tiempo de compilación como símbolos, átomos, expresiones sexuales, etc. Puede evaluarlas, pero usar eval se considera una mala práctica por varios motivos.

Sin embargo, podemos hacer un buen trabajo en tiempo de ejecución.

  • Lo que me gustaría asegurar es que: la etiqueta siempre está ahí,
  • y puede tomar solo uno de los valores especificados
  • y el valor correspondiente es consistentemente del mismo tipo / comportamiento
  • y no puede ser nulo
  • y hay una manera fácil de ver que me encargué de todos los casos en el código.

Mi estrategia será convertir todo lo que normalmente es estático (en Haskell) a tiempo de ejecución. Vamos a escribir algún código.

;; let us define a union "type" (static type to runtime value) (def either-string-number {:left java.lang.String :right java.lang.Number}) ;; a constructor for a given type (defn mk-value-of-union [union-type tag value] (assert (union-type tag)) ; tag is valid (assert (instance? (union-type tag) value)) ; value is of correct type (assert value) {:tag tag :value value :union-type union-type}) ;; "conditional" to ensure that all the cases are handled ;; take a value and a map of tags to functions of one argument ;; if calls the function mapped to the appropriate tag (defn union-case-fn [union-value tag-fn] ;; assert that we handle all cases (assert (= (set (keys tag-fn)) (set (keys (:union-type union-value))))) ((tag-fn (:tag union-value)) (:value union-value))) ;; extra points for wrapping this in a macro ;; example (def j (mk-value-of-union either-string-number :right 2)) (union-case-fn j {:left #(println "left: " %) :right #(println "right: " %)}) => right: 2 (union-case-fn j {:left #(println "left: " %)}) => AssertionError Assert failed: (= (set (keys tag-fn)) (set (keys (:union-type union-value))))

Este código utiliza las siguientes construcciones idiomáticas de Clojure:

  • Programación basada en datos: cree una estructura de datos que represente el "tipo". Este valor es inmutable y de primera clase y tiene todo el lenguaje disponible para implementar la lógica con él. Esto es algo que no creo que Haskell pueda hacer: manipular tipos en tiempo de ejecución.
  • Usando mapas para representar valores.
  • Programación de orden superior: pasar un mapa de fns a otra función.

Opcionalmente, podría usar protocolos si está utilizando Either para polimorfismo. De lo contrario, si está interesado en la etiqueta, algo de la forma {:tag :left :value 123} es la más idiomática. A menudo verá algo como esto:

;; let''s say we have a function that may generate an error or succeed (defn somefunction [] ... (if (some error condition) {:status :error :message "Really bad error occurred."} {:status :success :result [1 2 3]})) ;; then you can check the status (let [r (somefunction)] (case (:status r) :error (println "Error: " (:message r)) :success (do-something-else (:result r)) ;; default (println "Don''t know what to do!")))

Editado Mi pregunta ahora es: ¿qué construcciones idiomáticas de Clojure se usan generalmente en lugar de tipos de suma en lenguajes de tipos estáticos? Consenso hasta el momento: use protocolos si el comportamiento puede ser unificado, use pares / mapas etiquetados de otra manera, ponga las afirmaciones necesarias en condiciones previas y posteriores.

Clojure proporciona muchas formas de expresar tipos de productos : vectores, mapas, registros ... pero, ¿cómo se representan los tipos de suma , también conocidos como uniones etiquetadas y registros de variantes? Algo como Either ab en Haskell o Either[+A, +B] en Scala.

Lo primero que me viene a la mente es un mapa con una etiqueta especial: {:tag :left :value a} , pero luego todo el código será contaminado con condicionales en (:tag value) y manejará casos especiales si es no está ahí ... Lo que me gustaría asegurar es que :tag siempre está ahí, y puede tomar solo uno de los valores especificados, y el valor correspondiente es consistentemente del mismo tipo / comportamiento y no puede ser nil , y ahí está Es una forma fácil de ver que me encargué de todos los casos en el código.

Puedo pensar en una macro en las líneas de defrecord , pero para los tipos de suma:

; it creates a special record type and some helper functions (defvariant Either left Foo right :bar) ; user.Either (def x (left (Foo. "foo"))) ;; factory functions for every variant ; #user.Either{:variant :left :value #user.Foo{:name "foo"}} (def y (right (Foo. "bar"))) ;; factory functions check types ; SomeException... (def y (right ^{:type :bar} ())) ; #user.Either{:variant :right :value ()} (variants x) ;; list of all possible options is intrinsic to the value ; [:left :right]

¿Ya existe algo como esto? ( Respondido: no ).


Al ser un lenguaje de tipo dinámico, los tipos en general son algo menos relevantes / importantes en Clojure que en Haskell / Scala. Realmente no necesita definirlos explícitamente , por ejemplo, ya puede almacenar valores de tipo A o tipo B en una variable.

Entonces, realmente depende de lo que estés tratando de hacer con estos tipos de suma. Es probable que esté realmente interesado en el comportamiento polimórfico basado en el tipo , en cuyo caso puede tener sentido definir un protocolo y dos tipos de registro diferentes que juntos den el comportamiento polimórfico de un tipo de suma:

(defprotocol Fooable (foo [x])) (defrecord AType [avalue] Fooable (foo [x] (println (str "A value: " (:avalue x))))) (defrecord BType [bvalue] Fooable (foo [x] (println (str "B value: " (:bvalue x))))) (foo (AType. "AAAAAA")) => A value: AAAAAA

Creo que esto proporcionará casi todos los beneficios que es probable que desee obtener de los tipos de suma.

Otras buenas ventajas de este enfoque:

  • Los registros y protocolos son muy idiomáticos en Clojure.
  • Excelente rendimiento (ya que el despacho de protocolo está fuertemente optimizado)
  • Puede agregar manejo para nulo en su protocolo (a través extend-protocol )

En general, los tipos de suma en idiomas tipificados dinámicamente se representan como:

  • pares etiquetados (por ejemplo, un tipo de producto con una etiqueta que representa al constructor)
  • Análisis de caso en la etiqueta en tiempo de ejecución para hacer un envío

En un lenguaje de tipo estático, la mayoría de los valores se distinguen por los tipos, lo que significa que no es necesario realizar un análisis de etiquetas en tiempo de ejecución para saber si tiene un Either o un Maybe . o un Right .

En una configuración de tipo dinámico, primero debe hacer un análisis de tipo de tiempo de ejecución (para ver qué tipo de valor tiene) y luego el análisis de caso del constructor (para ver qué valor de valor tiene).

Una forma es asignar una etiqueta única para cada constructor de cada tipo.

En cierto modo, puede pensar que la escritura dinámica coloca todos los valores en un solo tipo de suma, aplazando todo el análisis de tipo a las pruebas de tiempo de ejecución.

Lo que me gustaría asegurar es que: la etiqueta siempre está ahí, y puede tomar solo uno de los valores especificados, y el valor correspondiente es consistentemente del mismo tipo / comportamiento y no puede ser nulo, y hay una manera fácil de Ver que me ocupé de todos los casos en el código.

Además, esta es una descripción de lo que haría un sistema de tipo estático.


La razón por la que esto funciona tan bien en algunos idiomas es que usted envía (generalmente por tipo) sobre el resultado, es decir, usa alguna propiedad (generalmente, tipo) del resultado para decidir qué hacer a continuación.

por lo que necesita ver cómo puede ocurrir el despacho en clojure.

  1. Caso especial nulo : el valor nil está incluido en varios lugares y se puede utilizar como parte "Ninguno" de "Tal vez". por ejemplo, if-let es muy útil.

  2. coincidencia de patrones - clojure base no tiene mucho soporte para esto, aparte de desestructurar secuencias, pero hay varias bibliotecas que sí lo hacen. ¿Ver reemplazo de Clojure para ADT y patrón de coincidencia? [ actualización : en los comentarios mnicky dice que está desactualizado y que debes usar core.match ]

  3. por tipo con OO - los métodos se seleccionan por tipo. para que pueda devolver diferentes subclases de un padre y llamar a un método que esté sobrecargado para realizar las diferentes operaciones que desee. Si viene de un fondo funcional que se sentirá muy extraño / torpe, pero es una opción.

  4. Etiquetas a mano : finalmente, puede usar case o case con etiquetas explícitas. de manera más útil, puede envolverlos en algún tipo de macro que funcione de la manera que desee.


No, no hay tal cosa en secreto a partir de ahora. Aunque puede implementarlo pero IMO, este tipo parece ser más adecuado para los lenguajes tipificados estáticamente y no le dará muchos beneficios en entornos dinámicos como clojure.


Sin la finalización de algo alucinante como el clojure mecanografiado , no creo que pueda evitar la comprobación de las afirmaciones en tiempo de ejecución.

Una característica menos conocida proporcionada por clojure que definitivamente puede ayudar con las comprobaciones de tiempo de ejecución es la implementación de condiciones previas y posteriores (consulte http://clojure.org/special_forms y una publicación de blog de fogus ). Creo que incluso podría usar una única función de envoltorio de orden superior con condiciones previas y posteriores para verificar todas sus afirmaciones sobre el código relevante. Eso evita el "problema de la contaminación" del control de tiempo de ejecución bastante bien.


Use un vector con la etiqueta como primer elemento de un vector y use core.match para destruir los datos etiquetados. Por lo tanto, para el ejemplo anterior, los datos "cualquiera" se codificarían como:

[:left 123] [:right "hello"]

Para luego destruirlo necesitarías referirte a core.match y usar:

(match either [:left num-val] (do-something-to-num num-val) [:right str-val] (do-something-to-str str-val))

Esto es más conciso que las otras respuestas.

Esta conversación de YouTube ofrece una explicación más detallada de por qué los vectores son deseables para codificar variantes en mapas. Mi resumen es que usar mapas para codificar variantes es problemático porque hay que recordar que el mapa es un "mapa etiquetado" y no un mapa normal. Para utilizar un "mapa etiquetado" correctamente, siempre debe hacer una búsqueda en dos etapas: primero la etiqueta, luego los datos basados ​​en la etiqueta. Si ( cuando ) olvida buscar la etiqueta en una variante codificada en el mapa u obtener las búsquedas de clave incorrectas para la etiqueta o los datos, obtendrá una excepción de puntero nulo que es difícil de localizar.

El video también cubre estos aspectos de las variantes codificadas por vectores:

  • Atrapando etiquetas ilegales.
  • Agregar una verificación estática, si lo desea, usando Typed Clojure .
  • Almacenando estos datos en Datomic .