¿Por qué se discriminan las uniones asociadas con la programación funcional?

30

En muchos años de programación OO, he entendido lo que son los sindicatos discriminados, pero nunca los extrañé realmente. Recientemente he estado haciendo una programación funcional en C # y ahora encuentro que sigo deseando tenerlos. Esto me desconcierta porque, a primera vista, el concepto de uniones discriminadas parece bastante independiente de la dicotomía funcional / OO.

¿Hay algo inherente en la programación funcional que hace que las uniones discriminadas sean más útiles de lo que serían en OO, o es que al obligarme a analizar el problema de una manera "mejor", simplemente he mejorado mis estándares y ahora exijo una mejor ¿modelo?

Andy
fuente
El problema de expresión en.wikipedia.org/wiki/Expression_problem podría ser relevante
xji
Realmente no es una respuesta adecuada a la pregunta, por lo que responderé como un comentario, pero el lenguaje de programación de Ceilán tiene tipos de unión y aparentemente se están arrastrando a otros lenguajes de paradigma OO / mixto: me vienen a la mente TypeScript y Scala. También las enumeraciones del lenguaje Java pueden usarse como una especie de implementación de uniones discriminadas.
Roland Tepp

Respuestas:

45

Las uniones discriminadas realmente brillan junto con la coincidencia de patrones, donde se selecciona un comportamiento diferente según los casos. Pero este patrón es fundamentalmente antitético a los principios de OO puro.

En OO puro, las diferencias de comportamiento deben definirse por los tipos (objetos) en sí mismos y encapsularse. Por lo tanto, la equivalencia con la coincidencia de patrones sería llamar a un método único en el objeto mismo, que luego se sobrecarga por los subtipos en cuestión para definir un comportamiento diferente. Inspeccionar el tipo de un objeto desde el exterior (que es lo que hace la coincidencia de patrones) se considera un antipatrón.

La diferencia fundamental es que los datos y el comportamiento están separados en la programación funcional, mientras que los datos y el comportamiento están encapsulados en OO.

Esta es la razón histórica. Un lenguaje como C # se está desarrollando desde un lenguaje OO clásico hasta un lenguaje de paradigmas múltiples al incorporar más y más funciones.

JacquesB
fuente
66
Un tipo de unión / suma discriminada no es como un árbol de herencia o múltiples clases que implementan una interfaz; Es un tipo con muchos tipos de valores. Tampoco evitan la encapsulación; el cliente no necesita saber que tiene múltiples tipos de valores. Considere una lista vinculada, que puede ser un nodo vacío o un valor y una referencia a otro nodo. Sin tipos de suma, terminas hackeando una unión discriminada de todas formas teniendo variables para el valor y la referencia, pero tratando un objeto con una referencia nula como un nodo vacío y pretendiendo que la variable de valor no existe.
Doval
2
En general, terminas incluyendo las variables para todos los tipos posibles de valores en una clase, más algún tipo de indicador o enumeración para decirte qué tipo de valor es esa instancia, y bailando alrededor de las variables que no corresponden a ese tipo.
Doval
77
@Doval Hay una codificación "estándar" de un tipo de datos algebraicos en clases al tener una interfaz / clase base abstracta que representa el tipo en general y al tener una subclase para cada caso del tipo. Para manejar la coincidencia de patrones, tiene un método que toma una función para cada caso, por lo que para una lista que tendría en la interfaz de nivel superior, List<A>el método B Match<B>(B nil, Func<A,List<A>,B> cons). Por ejemplo, este es exactamente el patrón que Smalltalk usa para los booleanos. Esto también es básicamente cómo lo maneja Scala. Que se usen varias clases es un detalle de implementación que no necesita ser expuesto.
Derek Elkins
3
@Doval: la comprobación explícita de referencias nulas tampoco se considera realmente idiomática
JacquesB
@JacquesB La verificación nula es un detalle de implementación. Para el cliente de la lista vinculada, generalmente hay un isEmptymétodo que verifica si la referencia al siguiente nodo es nula.
Doval
35

Habiendo programado en Pascal y Ada antes de aprender programación funcional, no asocio uniones discriminadas con programación funcional.

Las uniones discriminadas son de alguna manera el doble de la herencia. El primero permite agregar fácilmente operaciones en un conjunto fijo de tipos (aquellos en la unión), y la herencia permite agregar fácilmente tipos con un conjunto fijo de operaciones. (Cómo agregar fácilmente ambos se llama problema de expresión ; ese es un problema especialmente difícil para los idiomas con un sistema de tipo estático).

Debido al énfasis de OO en los tipos y al doble énfasis de la programación funcional en las funciones, los lenguajes de programación funcional tienen una afinidad natural por los tipos de unión y ofrecen estructuras sintácticas para facilitar su uso.

Un programador
fuente
7

Las técnicas de programación imperativa, como se usan a menudo en OO, se basan a menudo en dos patrones:

  1. Tener éxito o lanzar una excepción,
  2. Vuelva nulla indicar "sin valor" o error.

El paradigma funcional generalmente evita ambos, prefiriendo devolver un tipo compuesto que indique la razón de éxito / fracaso o valor / no valor.

Los sindicatos discriminados se ajustan al proyecto de ley para estos tipos compuestos. Por ejemplo, en primera instancia, puede regresar true, o alguna estructura de datos que describa la falla. En el segundo caso, una unión que contiene un valor, o none, niletc. El segundo caso es tan común que muchos lenguajes funcionales tienen un tipo "quizás" u "opción" incorporado para representar ese valor / ninguna unión.

Al cambiar a un estilo funcional con, por ejemplo, C #, encontrará rápidamente la necesidad de estos tipos compuestos. void/throwy nullsimplemente no me siento bien con ese código. Y los sindicatos discriminados (DU) se ajustan bien a la ley. Por lo tanto, te encontraste deseándolos, al igual que muchos de nosotros.

La buena noticia es que hay muchas bibliotecas por ahí que modelan DUs en, por ejemplo, C # (eche un vistazo a mi propia biblioteca Succinc <T> , por ejemplo).

David Arno
fuente
2

Los tipos de suma serían generalmente menos útiles en los lenguajes OO convencionales, ya que están resolviendo un tipo de problema similar al subtipo OO. Una forma de verlos es que ambos manejan opensubtipos, pero OO es, es decir, uno puede agregar subtipos arbitrarios a un tipo primario y los tipos de suma son, closedes decir, uno determina por adelantado qué subtipos son válidos.

Ahora, muchos lenguajes OO combinan el subtipo con otros conceptos, como estructuras heredadas, polimorfismo, mecanografía de referencia, etc. para que sean generalmente más útiles. Una consecuencia es que tienden a ser más trabajos de configuración (con clases y constructores y demás), por lo que tienden a no usarse para cosas como Resultsy Options y así sucesivamente hasta que la tipificación genérica se hizo común.

También diría que el enfoque en las relaciones del mundo real que la mayoría de la gente aprendió cuando comenzaron la programación OO, por ejemplo, Dog isa Animal, significaba que Integer is a Result o Error isa Result parece un poco extraño. Aunque las ideas son bastante similares.

En cuanto a por qué los lenguajes funcionales podrían preferir la escritura cerrada a la abierta, una posible razón es que tienden a preferir la coincidencia de patrones. Esto es útil para el polimorfismo de la función, pero también funciona muy bien con tipos cerrados ya que el compilador puede verificar estáticamente que la coincidencia cubre todos los subtipos. Esto puede hacer que el lenguaje se sienta más consistente, aunque no creo que haya ningún beneficio inherente (podría estar equivocado).

Alex
fuente
Por cierto: Scala ha cerrado la herencia. sealedsignifica "solo se puede extender en la misma unidad de compilación", lo que le permite cerrar el conjunto de subclases en tiempo de diseño.
Jörg W Mittag
-3

Swift felizmente usa sindicatos discriminatorios, excepto que los llama "enumeraciones". Las enumeraciones son una de las cinco categorías fundamentales de objetos en Swift, que son clase, estructura, enumeración, tupla y cierre. Los opcionales Swift son enumeraciones que son uniones discriminatorias, y son absolutamente esenciales para cualquier código Swift.

Por lo tanto, la premisa de que "los sindicatos discriminatorios están asociados con la programación funcional" es errónea.

gnasher729
fuente