¿Qué problema resuelven los tipos de datos algebraicos?

18

Advertencia justa, soy nuevo en la programación funcional, por lo que puedo tener muchos supuestos erróneos.

He estado aprendiendo sobre tipos algebraicos. Muchos lenguajes funcionales parecen tenerlos, y son bastante útiles junto con la coincidencia de patrones. Sin embargo, ¿qué problema resuelven realmente? Puedo implementar un tipo algebraico aparentemente (más o menos) en C # como este:

public abstract class Option { }
public class None : Option { }
public class Some<T> : Option
{
    public T Value { get; set; }
}

var result = GetSomeValue();
if(result is None)
{
}
else
{
}

Pero creo que la mayoría estaría de acuerdo en que esto es una bastarización de la programación orientada a objetos, y nunca deberías hacerlo. Entonces, ¿la programación funcional solo agrega una sintaxis más limpia que hace que este estilo de programación parezca menos asqueroso? ¿Qué más me estoy perdiendo?

CondiciónRacer
fuente
66
La programación funcional es un paradigma diferente a la programación orientada a objetos.
Basile Starynkevitch
@BasileStarynkevitch Me doy cuenta, pero hay lenguajes como F # que son un poco de ambos. La pregunta no es tanto sobre lenguajes funcionales, sino más sobre qué problemas resuelven los tipos de datos algebraicos.
ConditionRacer
77
¿Qué sucede cuando defino class ThirdOption : Option{}y le doy un lugar new ThirdOption()donde esperaba Someo None?
amon
1
Los idiomas de @amon que tienen tipos de suma generalmente tienen formas de no permitirlo. Por ejemplo, Haskell define data Maybe a = Just a | Nothing(equivalente a data Option a = Some a | Noneen su ejemplo): no puede agregar un tercer caso post-hoc. Si bien puedes emular tipos de suma en C # de la manera que has mostrado, no es el más bonito.
Martijn
1
Creo que es menos "qué problema resuelven los TDA" y más como "los TDA son una forma diferente de abordar las cosas".
MathematicalOrchid

Respuestas:

44

Las clases con interfaces y herencia presentan un mundo abierto: cualquiera puede agregar un nuevo tipo de datos. Para una interfaz determinada, puede haber clases que la implementen en todo el mundo, en diferentes archivos, en diferentes proyectos, en diferentes compañías. Facilitan agregar casos a las estructuras de datos, pero debido a que las implementaciones de la interfaz están descentralizadas, es difícil agregar un nuevo método a la interfaz. Una vez que una interfaz es pública, se congela básicamente. Nadie conoce todas las implementaciones posibles.

Los tipos de datos algebraicos son duales a eso, están cerrados . Todos los casos de los datos se enumeran en un solo lugar y las operaciones no solo pueden enumerar las variantes de forma exhaustiva, sino que se les recomienda que lo hagan. Consecuentemente, escribir una nueva función que opera en un tipo de datos algebraicos es trivial: solo escriba la maldita función. A cambio, agregar nuevos casos es complicado porque necesita revisar básicamente toda la base de código y extender cada uno match. Similar a la situación con las interfaces, en la biblioteca estándar Rust, agregar una nueva variante es un cambio radical (para tipos públicos).

Estos son dos lados del problema de expresión . Los tipos de datos algebraicos son una solución incompleta para ellos, pero también lo es la POO. Ambos tienen ventajas dependiendo de cuántos casos de datos hay, con qué frecuencia cambian esos casos y con qué frecuencia se amplían o cambian las operaciones. (Es por eso que muchos lenguajes modernos proporcionan ambos, o algo similar, o van directamente a mecanismos más potentes y más complicados que intentan subsumir ambos enfoques).


fuente
¿Obtiene un error del compilador si no puede actualizar una coincidencia, o es solo en el tiempo de ejecución que lo descubre?
Ian
66
@Ian La mayoría de los lenguajes funcionales se escriben estáticamente y verifican la exhaustividad de la coincidencia de patrones. Sin embargo, si hay un patrón de "capturar todo", el compilador está contento incluso si la función tendría que ocuparse del nuevo caso para hacer su trabajo. Además, debe volver a compilar todo el código dependiente, no puede compilar solo una biblioteca y volver a vincularla en una aplicación ya construida.
Además, verificar estáticamente que un patrón es exhaustivo es costoso en general. Resuelve el problema SAT en el mejor de los casos y, en el peor de los casos, el lenguaje permite predicados arbitrarios que lo hacen indecidible.
Usr
@usr Sin lenguaje Soy consciente de que intenta una solución perfecta, o el compilador entiende que es exhaustivo o se ve obligado a agregar un caso general en el que se bloquea y se quema. Sin embargo, no sé de una relación con SAT, ¿tiene un enlace a una reducción? En cualquier caso, para el código real escrito en programas reales, la verificación de exhaustividad es una caída en el cubo.
Imagina que estás haciendo coincidir N booleanos. Luego agrega cláusulas de coincidencia como (a, b, _, d, ...). El caso general es entonces! Cláusula1 &&! Cláusula2 && .... Eso me parece SAT.
usr
12

Entonces, ¿la programación funcional solo agrega una sintaxis más limpia que hace que este estilo de programación parezca menos asqueroso?

Esa es una simplificación quizás, pero sí.

¿Qué más me estoy perdiendo?

Seamos claros sobre qué tipos de datos algebraicos son (resumiendo este excelente enlace de Aprenda usted como Haskell):

  • Un tipo de suma que dice "este valor puede ser A o B".
  • Un tipo de producto que dice "este valor es tanto A como B".

tu ejemplo solo funciona realmente con el primero.

Quizás lo que se está perdiendo es que al proporcionar estas dos operaciones básicas, los lenguajes funcionales le permiten construir todo lo demás. C # tiene estructuras, clases, enumeraciones, genéricos además de esos y montones de reglas para gobernar cómo se comportan estas cosas.

Combinado con alguna sintaxis para ayudar, los lenguajes funcionales pueden descomponer las operaciones en estos dos caminos, proporcionando un enfoque limpio, simple y elegante para los tipos.

¿Qué problema resuelven los tipos de datos algebraicos?

Resuelven el mismo problema que cualquier otro tipo de sistema: "¿qué valores son legales para usar aquí?" - Simplemente toman un enfoque diferente.

Telastyn
fuente
44
Su última oración es algo así como decirle a alguien que "los barcos resuelven el mismo problema que los aviones: transporte: simplemente adoptan un enfoque diferente". Es una afirmación completamente correcta, y también bastante inútil.
Mehrdad
@mehrdad: creo que eso es exagerar un poco.
Telastyn
1
Sin duda, comprende los tipos de datos algebraicos muy bien, pero su lista con viñetas ("Un tipo de suma es ..." y "Un tipo de producto es ...") se parece más a una descripción de tipos de unión e intersección, que no son casi lo mismo que suma y tipos de productos.
pyon
6

Puede sorprenderle saber que la coincidencia de patrones no se considera la forma más idiomática de trabajar con Opciones. Consulte la documentación de las Opciones de Scala para obtener más información al respecto. No estoy seguro de por qué tantos tutoriales de FP fomentan este uso.

Principalmente, lo que falta es que hay un montón de funciones creadas para facilitar el trabajo con las Opciones. Considere el ejemplo principal de los documentos de Scala:

val name: Option[String] = request getParameter "name"
val upper = name map { _.trim } filter { _.length != 0 } map { _.toUpperCase }
println(upper getOrElse "")

Observe cómo mapy le filterpermite encadenar operaciones en Opciones, sin tener que verificar en cada punto si tiene Noneo no. Luego, al final, se utiliza getOrElsepara especificar un valor predeterminado. En ningún momento estás haciendo algo "asqueroso" como verificar tipos. Cualquier verificación de tipo inevitable se realiza internamente en la biblioteca. Haskell tiene su propio conjunto de funciones análogas, sin mencionar el gran conjunto de funciones que funcionará en cualquier mónada o functor.

Otros tipos de datos algebraicos tienen sus propias formas idiomáticas de trabajar con ellos, y la coincidencia de patrones es baja en el tótem en la mayoría de los casos. Del mismo modo, cuando crea sus propios tipos, se espera que proporcione funciones similares para trabajar con ellos.

Karl Bielefeldt
fuente