¿Es la coincidencia de patrones con tipos de diseño idiomático o deficiente?

18

Parece que el código F # a menudo coincide con patrones contra tipos. Ciertamente

match opt with 
| Some val -> Something(val) 
| None -> Different()

Parece común.

Pero desde una perspectiva OOP, eso se parece mucho al flujo de control basado en una verificación de tipo de tiempo de ejecución, que generalmente estaría mal visto. Para explicarlo, en OOP probablemente preferirías usar la sobrecarga:

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

Este es ciertamente más código. OTOH, me parece que mi OOP-y tiene ventajas estructurales:

  • la extensión a una nueva forma de Tes fácil;
  • No tengo que preocuparme por encontrar la duplicación del flujo de control de elección de ruta; y
  • la elección de la ruta es inmutable en el sentido de que una vez que tengo una Fooen la mano, nunca necesito preocuparme por Bar.Route()la implementación

¿Existen ventajas para la coincidencia de patrones con los tipos que no veo? ¿Se considera idiomático o es una capacidad que no se usa comúnmente?

Larry OBrien
fuente
3
¿Qué sentido tiene ver un lenguaje funcional desde una perspectiva OOP? De todos modos, el poder real de la coincidencia de patrones viene con patrones anidados. Solo es posible verificar el constructor más externo, pero de ninguna manera es toda la historia.
Ingo
Esto ... But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.suena demasiado dogmático. A veces, desea separar sus operaciones de su jerarquía: quizás 1) no puede agregar una operación a una jerarquía b / c que no posee la jerarquía; 2) las clases que desea tener la operación no coinciden con su jerarquía; 3) puede agregar la operación a su jerarquía, pero no quiere b / c no quiere saturar la API de su jerarquía con un montón de basura que la mayoría de los clientes no usan.
44
Solo para aclarar, Somey Noneno son tipos. Ambos son constructores cuyos tipos son forall a. a -> option ay forall a. option a(perdón, no estoy seguro de cuál es la sintaxis para las anotaciones de tipo en F #).

Respuestas:

20

Tiene razón en que las jerarquías de clases OOP están muy relacionadas con las uniones discriminadas en F # y que la coincidencia de patrones está muy relacionada con las pruebas de tipo dinámico. De hecho, así es como F # compila uniones discriminadas a .NET.

En cuanto a la extensibilidad, hay dos lados del problema:

  • OO le permite agregar nuevas subclases, pero dificulta agregar nuevas funciones (virtuales)
  • FP le permite agregar nuevas funciones, pero dificulta agregar nuevos casos de unión

Dicho esto, F # le dará una advertencia cuando omita un caso en la coincidencia de patrones, por lo que agregar nuevos casos sindicales en realidad no es tan malo.

Con respecto a la búsqueda de duplicaciones en la elección de la raíz: F # le dará una advertencia cuando tenga una coincidencia duplicada, por ejemplo:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

El hecho de que "la elección de ruta es inmutable" también podría ser problemático. Por ejemplo, si desea compartir la implementación de una función entre Fooy Barcasos, pero hacer algo más para el Zoocaso, puede codificarlo fácilmente utilizando la coincidencia de patrones:

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

En general, FP está más enfocado en diseñar primero los tipos y luego agregar funciones. Por lo tanto, realmente se beneficia del hecho de que puede ajustar sus tipos (modelo de dominio) en un par de líneas en un solo archivo y luego agregar fácilmente las funciones que operan en el modelo de dominio.

Los dos enfoques: OO y FP son bastante complementarios y ambos tienen ventajas y desventajas. Lo complicado (desde la perspectiva OO) es que F # generalmente usa el estilo FP como predeterminado. Pero si realmente hay más necesidad de agregar nuevas subclases, siempre puede usar interfaces. Pero en la mayoría de los sistemas, igualmente necesita agregar tipos y funciones, por lo que la elección realmente no importa tanto, y usar uniones discriminadas en F # es mejor.

Recomiendo esta gran serie de blogs para más información.

Tomás Petricek
fuente
3
Aunque tiene razón, me gustaría agregar que esto no es tanto un problema de OO vs FP como es un problema de objetos versus tipos de suma. Dejando de lado la obsesión de OOP con ellos, no hay nada en los objetos que los haga no funcionales. Y si salta a través de suficientes aros, también puede implementar tipos de suma en los idiomas principales de OOP (aunque no será bonito).
Doval
1
"Y si saltas a través de suficientes aros, también puedes implementar tipos de suma en los lenguajes principales de OOP (aunque no será bonito)". -> Supongo que terminarás con algo similar a cómo se codifican los tipos de suma F # en el sistema de tipos de .NET :)
Tarmil
7

Ha observado correctamente que la coincidencia de patrones (esencialmente una declaración de interruptor sobrealimentado) y el despacho dinámico tienen similitudes. También coexisten en algunos idiomas, con un resultado muy agradable. Sin embargo, hay ligeras diferencias.

Podría usar el sistema de tipos para definir un tipo que solo puede tener un número fijo de subtipos:

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

Habrá que nunca ser otro subtipo de Boolo Option, por lo subclases no parece ser útiles (algunos idiomas como el Scala tienen una noción de la subclasificación que puede manejar esto - una clase puede ser marcado como fuera de “final” de la unidad de compilación actual, pero subtipos se puede definir dentro de esta unidad de compilación).

Debido a que los subtipos de un tipo como Optionahora se conocen estáticamente , el compilador puede advertir si olvidamos manejar un caso en nuestra coincidencia de patrones. Esto significa que una coincidencia de patrones es más como un downcast especial que nos obliga a manejar todas las opciones.

Además, el despacho de método dinámico (que se requiere para OOP) también implica una verificación del tipo de tiempo de ejecución, pero de un tipo diferente. Por lo tanto, es bastante irrelevante si hacemos este tipo de verificación explícitamente a través de una coincidencia de patrones o implícitamente a través de una llamada a un método.

amon
fuente
"Esto significa que una coincidencia de patrones es más como un downcast especial que nos obliga a manejar todas las opciones". De hecho, creo que (siempre y cuando solo coincidan con constructores y no con valores o estructuras anidadas) es isomorfo a colocando un método virtual abstracto en la superclase.
Julio
2

La coincidencia de patrones F # generalmente se realiza con una unión discriminada en lugar de con clases (y, por lo tanto, técnicamente no es una verificación de tipo) Esto permite que el compilador le avise cuando no haya encontrado casos en una coincidencia de patrones.

Otra cosa a tener en cuenta es que, en un estilo funcional, organiza las cosas por funcionalidad en lugar de por datos, por lo que las coincidencias de patrones le permiten reunir las diferentes funcionalidades en un solo lugar en lugar de estar dispersas entre las clases. Esto también tiene la ventaja de que puede ver cómo se manejan otros casos justo al lado de donde necesita hacer sus cambios.

Agregar una nueva opción se ve así:

  1. Agregue una nueva opción a su sindicato discriminado
  2. Repara todas las advertencias en coincidencias de patrones incompletos
N / A
fuente
2

Parcialmente, lo ve más a menudo en la programación funcional porque usa tipos para tomar decisiones con más frecuencia. Me doy cuenta de que probablemente hayas elegido ejemplos más o menos al azar, pero el OOP equivalente a tu ejemplo de coincidencia de patrones se vería más a menudo:

if (opt != null)
    opt.Something()
else
    Different()

En otras palabras, es relativamente raro usar polimorfismo para evitar cosas rutinarias como cheques nulos en OOP. Al igual que un programador OO no crea un objeto nulo en cada pequeña situación, un programador funcional no siempre sobrecarga una función, especialmente cuando sabe que su lista de patrones está garantizada como exhaustiva. Si usa el sistema de tipos en más situaciones, verá que se usa de formas a las que no está acostumbrado.

Por el contrario, la programación idiomática equivalente funcional a su ejemplo POO lo más probable es que no utilice la coincidencia de patrones, pero tendría fooRoutey barRoutefunciones que van pasando como argumentos al código de llamada. Si alguien usó la coincidencia de patrones en esa situación, generalmente se consideraría incorrecto, al igual que alguien que cambia los tipos se consideraría incorrecto en OOP.

Entonces, ¿cuándo se considera que la coincidencia de patrones es un buen código de programación funcional? Cuando está haciendo más que solo mirar los tipos, y al extender los requisitos no requerirá agregar más casos. Por ejemplo, Some valno solo comprueba que opttiene tipo Some, sino que también se une valal tipo subyacente para un uso con tipo seguro en el otro lado del ->. Sabes que lo más probable es que nunca necesites un tercer caso, por lo que es un buen uso.

La coincidencia de patrones puede parecerse superficialmente a una declaración de cambio orientada a objetos, pero están sucediendo muchas más cosas, especialmente con patrones más largos o anidados. Asegúrese de tener en cuenta todo lo que está haciendo antes de declararlo equivalente a un código OOP mal diseñado. A menudo, está manejando sucintamente una situación que no puede representarse limpiamente en una jerarquía de herencia.

Karl Bielefeldt
fuente
Sé que sabes esto, y probablemente se te pasó por la cabeza mientras escribías tu respuesta, pero ten en cuenta que Somey Noneno son tipos, por lo que no coinciden los patrones en los tipos. Puede emparejar patrones en constructores del mismo tipo . Esto no es como preguntar "instanceof".
Andres F.