2012-06-08 10 views
19

Editado. Mi pregunta ahora es: ¿qué construcciones idiomáticas de Clojure se usan generalmente en lugar de tipos de suma en los idiomas de tipos estáticos? Consenso hasta el momento: utilice protocolos si el comportamiento se puede unificar, use pares/mapas etiquetados de lo contrario, ponga los asertos necesarios en las condiciones previas y posteriores.Forma idiomática para representar el tipo de suma (bien a b) en Clojure

Clojure ofrece muchas maneras de expresar tipos de productos : vectors, mapas, registros ..., pero ¿cómo se representa sum types, también conocido como los sindicatos y los registros marcados variantes? Algo así como Either a b 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 va a estar contaminado con condicionales en (:tag value) y manejar casos especiales si no está allí ... Lo que ' Me gustaría asegurarme de que :tag está siempre allí, 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 hay una manera fácil de ver que tomé cuidado de todos los casos en el código.

No 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] 

existe ya una cosa así? (Respondido: no).

+1

una gran respuesta al LispCast: http://www.lispcast.com/idiomatic-way-to-represent-either – sastanin

Respuesta

16

¿cómo se representan los tipos de suma, también conocidos como uniones con etiquetas y registros de variantes? Algo así como Either a b en Haskell o Either[+A, +B] en Scala.

Either tiene dos usos: para devolver un valor de uno de dos tipos o a retorno dos valores del mismo tipo que deben tener diferentes semántica sobre la base de 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 tipo Haskell. Con un sistema de tipo dinámico, puede devolver valores de cualquier tipo que desee. Either no es necesario.

El segundo uso es significativo, pero se puede lograr simplemente en dos (o más) formas:

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

Lo que me gustaría para asegurarse, 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 allí es una manera fácil de ver que yo cuidé todos los casos en el código.

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

El motivo por el cual una macro no funcionará es que al momento de la expansión de macros, 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, sexpresiones, etc. Puede puede eval se considera, pero el uso de eval se considera una mala práctica por un número de razones .

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

  • Lo que me gustaría para asegurar, es que: la etiqueta siempre está ahí,
  • y puede tomar sólo uno de los valores especificados
  • y correspondiente valor es consistente 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.

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

;; 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 la siguiente Clojure idiomática construye:

  • programación basada en datos: crear una estructura de datos que representa 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.
  • Uso de mapas para representar valores.
  • Programación de orden superior: pasando un mapa de fns a otra función.

De manera opcional, puede usar protocolos si está usando Either para polimorfismo. De lo contrario, si está interesado en la etiqueta, algo del formulario {:tag :left :value 123} es el más idiomático.Es frecuente encontrar que 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!"))) 
+0

Gracias. Cubre las características generalmente proporcionadas por los tipos de suma. La pregunta es, ¿es _idiomatic_ Clojure? – sastanin

+1

Respondido en la respuesta. –

2

No, no hay tal cosa en Clojure a partir de ahora. Aunque puede implementarlo pero IMO este tipo parece ser más adecuado para los lenguajes tipados estáticamente y no le dará mucho beneficio en un entorno dinámico como clojure.

4

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

, así que debe ver cómo puede ocurrir el despacho en clojure.

  1. nula caso especial - el valor nil es especial con carcasa en diversos lugares y se puede utilizar como "Ninguno" parte de "Tal vez". por ejemplo, if-let es muy útil.

  2. coincidencia de patrón - base clojure no tiene mucho soporte para esto, aparte de las secuencias de desestructuración, pero hay varias bibliotecas que lo hacen. ver Clojure replacement for ADTs and Pattern Matching? [actualización: en los comentarios mnicky dice que es obsoleta y se debe utilizar core.match]

  3. por tipo con OO - métodos son seleccionados por tipo. para que pueda devolver diferentes subclases de un padre y llamar a un método que está sobrecargado para hacer las diferentes operaciones que desee. si vienes de un entorno funcional que se sentirá muy extraño/torpe, pero es una opción.

  4. etiquetas a mano - Por último, se pueden utilizar case o cond con etiquetas explícitas. de forma más útil, puede envolverlos en algún tipo de macro que funcione de la manera que desee.

+0

Esta lista de posibles estrategias de despacho es muy interesante cuando pienso en la implementación de registros variantes con la mano . Despachar por tipo parece ser más interesante porque también funciona para objetos Java, pero se puede sobrescribir con metadatos ': type' para (casi) todo lo demás. – sastanin

+0

eh, no sabía acerca de: tipo. Gracias. –

+1

Esa pregunta ASÍ sobre la coincidencia de patrones está desactualizada. Ver [core.match] (https://github.com/clojure/core.match) ... – mnicky

6

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

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

En un lenguaje estáticamente tipado, la mayoría de los valores se distinguen por tipos, lo que significa que no necesita hacer análisis de etiquetas en tiempo de ejecución para saber si tiene Either o Maybe - así que solo mira la etiqueta para saber si es Left o Right.

En una configuración de tipeo dinámico, primero debe hacer 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é sabor tiene).

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

En cierto modo, puede pensar en el tipado dinámico como poner todos los valores en un tipo de suma única, posponiendo todo el análisis de tipo a las pruebas de tiempo de ejecución.


Lo que me gustaría para asegurar, es que: la etiqueta siempre está ahí, y puede tomar sólo uno de los valores especificados, y el valor correspondiente es consistente del mismo tipo/comportamiento y no puede haber nada, y hay una manera fácil de ver que me ocupé de todos los casos en el código.

Como un lado, esto es más o menos una descripción de lo que haría un sistema de tipo estático.

4

Al ser un lenguaje de tipos dinámicos, tipos, en general, son algo menos relevante/importante en Clojure de lo que son en Haskell/Scala. Usted realmente no necesita definirlos explícitamente - por ejemplo, ya puede almacenar valores de tipo A o tipo B en una variable.

Así que realmente depende de lo que intentes hacer con estos tipos de suma. Es probable que usted está realmente interesado en comportamiento polimórfico basado en el tipo, en cuyo caso puede tener sentido para definir un protocolo y dos tipos de registros diferentes que juntos dan el comportamiento polimórfico de un tipo 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 ofrecerá casi todos los beneficios que probablemente desee de los tipos de suma.

Otras ventajas agradables de este enfoque:

  • registros y protocolos son muy idiomático en Clojure
  • Excelente rendimiento (desde el despacho protocolo está muy optimizado)
  • Puede agregar el manejo de cero en el protocolo (a través de extend-protocol)
+0

Gracias. Esto ayuda cuando los valores tienen un comportamiento unificador, pero no ayuda cuando el comportamiento es diferente (digamos que el valor es "mensaje de error" o doble). En mi trabajo, puedo salirme con protocolos. – sastanin

+0

@sastanin: este enfoque funcionará bien para situaciones donde los valores son de tipos completamente diferentes: puede extender el protocolo por separado a java.lang.String y java.lang.Double por ejemplo. La única situación en la que no funcionará es cuando necesita despachar en un dispositivo que no sea el tipo (pero luego siempre puede ajustar un tipo de registro como en el ejemplo anterior) – mikera

4

sin la realización de algo alucinante como typed clojure no creo que ca n evitar el control del tiempo de ejecución de las aserciones.

Una característica menos conocida proporcionada por clojure que definitivamente puede ayudar con los controles de tiempo de ejecución es la implementación de condiciones previas y posteriores (ver http://clojure.org/special_forms y a blog post by fogus). Creo que incluso podría usar una única función de contenedor de orden superior con condiciones previas y posteriores para verificar todas sus afirmaciones en el código correspondiente. Eso evita el "problema de contaminación" de control de tiempo de ejecución muy bien.

+0

Gracias por señalar ': pre' y': post' condiciones – sastanin

+1

Hace un par de años: Typed Clojure ahora lo hace sencillo. https://github.com/clojure/core.typed –

2

Use un vector con la etiqueta como primer elemento en un vector y use core.match para desestructurar los datos etiquetados. Por tanto, para el ejemplo anterior, el "o bien" datos sería codificado como:

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

Para entonces desestructuran que se necesita para hacer referencia a core.match y uso:

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

Esto es más concisa que el otro respuestas

This youtube talk da una explicación más detallada de por qué los vectores son deseables para las variantes de codificación sobre los mapas. Mi resumen es que usar mapas para codificar variantes es problemático porque debes recordar que el mapa es un "mapa etiquetado", no un mapa normal. Para usar un "mapa etiquetado" correctamente, debe siempre hacer una búsqueda en dos etapas: primero la etiqueta, luego los datos basados ​​en la etiqueta. Si (cuando) se olvidó de buscar la etiqueta en una variante codificada en el mapa u obtener las búsquedas de claves incorrectas para la etiqueta o los datos, obtendrá una excepción de puntero nulo que es difícil de rastrear.

El video también cubre estos aspectos de variantes del vector codificado:

  • Atrapar etiquetas ilegales.
  • Agregar comprobación estática, si lo desea, utilizando Typed Clojure.
  • Almacenando esta información en Datomic.
Cuestiones relacionadas