Cuándo usar una Unión Discriminada vs Tipo de registro en F#
record discriminated-union (4)
Estoy tratando de aclarar los conceptos básicos de F # antes de pasar a los ejemplos complejos. El material que estoy aprendiendo ha introducido los tipos Discriminate Unions y Record. He revisado el material para ambos, pero todavía no está claro por qué usaríamos uno sobre el otro.
La mayoría de los ejemplos de juguetes que he creado parecen ser implementables en ambos. Los registros parecen estar muy cerca de lo que yo considero un objeto en C #, pero estoy tratando de evitar confiar en la asignación a c # como una forma de entender F #
Asi que...
¿Hay razones claras para usar uno sobre el otro?
¿Hay ciertos casos canónicos en los que se aplica?
¿Existen ciertas funcionalidades disponibles en una, pero no en la otra?
Piense en ello como un registro es ''y'', mientras que una unión discriminada es ''o''. Esta es una cadena y un int:
type MyRecord = { myString: string
myInt: int }
Si bien este es un valor que es una cadena o un int, pero no ambos:
type MyUnion = | Int of int
| Str of string
Este juego ficticio puede estar en la pantalla de título, en el juego o mostrando la puntuación final, pero solo una de esas opciones.
type Game =
| Title
| Ingame of Player * Score * Turn
| Endgame of Score
Si proviene de C #, puede entender los registros como clases selladas con valores agregados:
- Inmutable por defecto
- Igualdad estructural por defecto.
- Fácil de emparejar patrón
- etc.
Uniones discriminadas codifican alternativas por ejemplo
type Expr =
| Num of int
| Var of int
| Add of Expr * Expr
| Sub of Expr * Expr
El DU anterior se lee como sigue: una expresión es un número entero o una variable, o una suma de dos expresiones o restas entre dos expresiones. Estos casos no pueden ocurrir simultáneamente.
Necesitas todos los campos para construir un registro. También puede utilizar DUs dentro de los registros y viceversa.
type Name =
{ FirstName : string;
MiddleName : string option;
LastName : string }
El ejemplo anterior muestra que el segundo nombre es opcional.
En F #, a menudo comienza a modelar datos con tuplas o registros. Cuando se requieren funcionalidades avanzadas, puede moverlas a clases.
Por otro lado, las uniones discriminadas se utilizan para modelar alternativas y relaciones mutuas exclusivas entre los casos.
Una forma (ligeramente defectuosa) de entender un DU es verlo como una "unión" elegante de C #, mientras que un registro es más como un objeto común (con múltiples campos independientes).
Otra forma de ver un DU es considerar un DU como una jerarquía de clases de dos niveles, donde el tipo de DU superior es una clase base abstracta y los casos de DU son subclases. Esta vista es realmente cercana a la implementación real de .NET, aunque este detalle está oculto por el compilador.
Use los registros (llamados tipos de productos en la teoría de la programación funcional) para datos complejos que se describen en varias propiedades, como un registro de base de datos o alguna entidad modelo:
type User = { Username : string; IsActive : bool }
type Body = {
Position : Vector2<double<m>>
Mass : double<kg>
Velocity : Vector2<double<m/s>>
}
Utilice uniones discriminadas (llamadas tipos de suma) para los valores de datos posibles para los cuales se pueden enumerar. Por ejemplo:
type NatNumber =
| One
| Two
| Three
...
type UserStatus =
| Inactive
| Active
| Disabled
type OperationResult<''T> =
| Success of ''T
| Failure of string
Tenga en cuenta que los valores posibles para un valor de unión discriminado también se excluyen mutuamente: un resultado para una operación puede ser un Success
o un Failure
, pero no ambos al mismo tiempo.
Podría usar un tipo de registro para codificar un resultado de una operación, como esto:
type OperationResult<''T> = {
HasSucceeded : bool
ResultValue : ''T
ErrorMessage : string
}
Pero en caso de falla de la operación, es ResultValue
no tiene sentido. Por lo tanto, la coincidencia de patrones en una versión de unión discriminada de este tipo se vería así:
match result with
| Success resultValue -> ...
| Failure errorMessage -> ...
Y si ajusta la versión del tipo de registro de nuestro tipo de operación, tendría menos sentido:
match result with
| { HasSucceeded = true; ResultValue = resultValue; ErrorMessage = _ } -> ...
| { HasSucceeded = false; ErrorMessage = errorMessage; ResultValue = _ } -> ...
Se ve detallado y torpe, y probablemente también sea menos eficiente. Creo que cuando tienes una sensación como esta es probablemente una pista de que estás usando una herramienta incorrecta para la tarea.