Métodos Maybe Monad en C#
En esta página:
En C# podemos trabajar con un contenedor llamado Maybe Monad, el cual puede representar un valor que puede existir o no, en este Post analizaré algunos métodos que facilitan el trabajo con Maybe Monad en C# y conforme vayamos avanzando se te hará más fácil la comprensión del trabajo con Maybe Monad en C#.
Antes de continuar con este Post te invito a escuchar el Podcast: “Herramientas Online Para El Trabajo En Equipo”:
Spotify:
Sound Cloud:
Bien ahora continuemos con el Post: Métodos Maybe Monad en C#.
Antes de pasar a los métodos consideremos el siguiente código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
using System; namespace MaybeAsAStruct { public struct Maybe<T> { private readonly T value; private readonly bool hasValue; private Maybe(T value) { this.value = value; hasValue = true; } public TResult Match<TResult>(Func<T, TResult> some, Func<TResult> none) { if (hasValue) return some(value); return none(); } public void Match(Action<T> some, Action none) { if (hasValue) { some(value); } else { none(); } } public static implicit operator Maybe<T>(T value) { if(value == null) return new Maybe<T>(); return new Maybe<T>(value); } public static implicit operator Maybe<T>(Maybe.MaybeNone value) { return new Maybe<T>(); } public bool intentarObtenerValor(out T value) { if (hasValue) { value = this.value; return true; } value = default(T); return false; } public Maybe<TResult> Map<TResult>(Func<T, TResult> convert) { if(!hasValue) return new Maybe<TResult>(); return convert(value); } public Maybe<TResult> Select<TResult>(Func<T, TResult> convert) { if (!hasValue) return new Maybe<TResult>(); return convert(value); } public Maybe<TResult> Bind<TResult>(Func<T, Maybe<TResult>> convert) { if (!hasValue) return new Maybe<TResult>(); return convert(value); } public Maybe<TResult> SelectMany<T2, TResult>( Func<T, Maybe<T2>> convert, Func<T, T2, TResult> finalSelect) { if (!hasValue) return new Maybe<TResult>(); var converted = convert(value); if (!converted.hasValue) return new Maybe<TResult>(); return finalSelect(value, converted.value); } public Maybe<T> Where(Func<T, bool> predicate) { if (!hasValue) return new Maybe<T>(); if (predicate(value)) return this; return new Maybe<T>(); } public T ValueOr(T defaultValue) { if (hasValue) return value; return defaultValue; } public T ValueOr(Func<T> defaultValueFactory) { if (hasValue) return value; return defaultValueFactory(); } public Maybe<T> ValueOrMaybe(Maybe<T> alternativeValue) { if (hasValue) return this; return alternativeValue; } public Maybe<T> ValueOrMaybe(Func<Maybe<T>> alternativeValueFactory) { if (hasValue) return this; return alternativeValueFactory(); } public T ValueOrThrow(string errorMessage) { if (hasValue) return value; throw new Exception(errorMessage); } } public static class Maybe { public class MaybeNone { } public static MaybeNone None { get; } = new MaybeNone(); public static Maybe<T> Some<T>(T value) { if (value == null) throw new ArgumentNullException(nameof(value)); return value; } } } |
Bueno ahora pasemos a los métodos Maybe.
ValueOr y ValueOrMaybe (Manejo de casos donde no hay un valor)
El método ValueOr se puede usar para brindar un valor predeterminado en caso de que el objeto Maybe no contenga un valor, por ejemplo:
1 2 3 4 5 6 7 8 |
static void miMetodo() { var mensajeError = GetErrorDescription(15) .ValueOr("Unknown error"); } |
El método GetErrorDescription devuelve un Maybe<string> que representa la descripción del error para el código específico. Retorna None (Maybe sin valor) en caso de que no haya una descripción definida para el código de error específico. El tipo de variable mensajeError es un string, específicamente un no Maybe <string>.
La variable mensajeError siempre tendrá un valor. Si el método GetErrorDescription retorna None, se devuelve el valor predeterminado “Unknown error” y se almacena dentro de la variable mensajeError, veamos el siguiente código:
1 2 3 4 5 |
var mensajeError = GetErrorDescription(15) .ValueOr(GetDefaultErrorMessage()); |
En el código anterior, el valor predeterminado se obtiene llamando a un método llamado GetDefaultErrorMessage. Este método GetDefaultErrorMessage siempre se llamará aquí, incluso si GetErrorDescription devuelve un valor. Esto puede ser un problema si GetDefaultErrorMessage afecta el rendimiento o si tiene efectos secundarios que solo queremos tener si GetErrorDescription devuelve None.
ValueOrThrow (Obtener el valor o lanzar un excepción si no hay un valor)
Consideremos el siguiente código:
1 2 3 4 5 6 7 8 |
static void miMetodo() { var contenidoRegistro = GetLogContents(1) .ValueOrThrow("No se puede obtener el contenido del registro"); } |
El método ValueOrThrow anterior provocará una excepción si GetLogContents devuelve None. Caso contrario devolverá el contenido del registro.
Otro valor (variaciones)
Podemos crear diferentes variaciones de los métodos ValueOr según el tipo de valor. Por ejemplo, podemos declarar ValueOrEmptyArray:
1 2 3 4 5 6 |
public static T[] ValueOrEmptyArray<T>(this Maybe<T[]> maybe) { return maybe.ValueOr(Array.Empty<T>()); } |
Otro ejemplo son los métodos de extensión ValueOrEmptyString:
1 2 3 4 5 6 |
public static string ValueOrEmptyString(this Maybe<string> maybe) { return maybe.ValueOr(string.Empty); } |
Uso de GetItemsWithValue
Veamos el siguiente código:
1 2 3 4 5 6 7 8 9 10 |
static void miMetodo() { List<string> contenidoRegistroMultiple = Enumerable.Range(1, 15) .Select(x => GetLogContents(x)) .GetItemsWithValue() .ToList(); } |
En el código anterior invocamos GetLogContents 15 veces, el método Select devuelve un enumerable de tipo IEnumerable <Maybe<string>>
GetItemsWithValue nos permite obtener un IEnumerable <string> que corresponde a los objetos que talvez tiene valores. Los que no tienen un valor no se incluirán en el enumerable devuelto.
Uso de IfAllHaveValues
Consideremos el siguiente código:
1 2 3 4 5 6 7 8 9 10 11 12 |
static void miMetodo() { List<string> contenidoRegistroMultiple = Enumerable.Range(1, 15) .Select(x => GetLogContents(x)) <span> //Signature: Maybe<IEnumerable<T>> IfAllHaveValues<T>(this IEnumerable<Maybe<T>> enumerable)</span> .IfAllHaveValues() .ValueOrThrow("Some logs are not available") .ToList(); } |
En el código anterior IfAllHaveValues devolverá None si alguno de los elementos enumerables no tiene valor. Si alguno de los 15 registros no está disponible, IfAllHaveValues devolvería None y ValueOrThrow arrojará una excepción.
La Signature IfAllHaveValues toma un IEnumerable <Maybe<T>> y retorna un Maybe <IEnumerable<T>>
Uso de ToAddIfHasValue
Consideremos el siguiente código:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void miMetodo() { Maybe<string> logMaybe = Maybe.Some("entry9"); var lista = new List<string>() { "entry1", logMaybe.ToAddIfHasValue(), "entry2" }; } |
En el código anterior, creamos una lista de Strings, queremos que la lista tenga “entrada1”, “entrada2”. Asimismo si logMaybe tiene un valor, queremos que su valor se incluya entre “entrada1” y “entrada2”.
La variable lista contiene una lista que tiene 2 o 3 entradas en su interior, dependiendo de si logMaybe tiene un valor. En el código anterior podemos ver que tiene el valor “entry9”.
En C# esto es posible porque la sintaxis del inicializador de lista es extensible. Puede tener un valor, digamos del tipo TValue, en la lista de inicialización siempre que haya un método con una Signature similar a:
1 2 3 4 |
# Signature (Firma) void Add(this TCollection collection, TValue value) |
Donde TCollection es el tipo de lista que estamos tratando de inicializar.
La siguiente versión de miMetodo es equivalente a la versión mostrada anteriormente:
1 2 3 4 5 6 7 8 9 10 11 12 |
static void Test16() { Maybe<string> logMaybe = Maybe.Some("entry9"); var lista = new List<string>(); list.Add("entrada1"); //List<T>.Add list.Add(logMaybe.ToAddIfHasValue()); //mi método (extensión) list.Add("entrada2"); //List<T>.Add } |
Tenemos el siguiente método Add:
1 2 3 4 5 6 7 8 9 10 11 |
public static void Add<T>( this ICollection<T> collection, AddIfHasValue<T> addIfHasValue) { if (addIfHasValue.Maybe.TryGetValue(out var value)) { collection.Add(value); } } |
El método ToAddIfHasValue nos permite ajustar un objeto Maybe dentro de un type especial AddIfHasValue<T>. En la primera versión de miMetodo, el valor devuelto por logMaybe.ToAddIfHasValue() es de tipo AddIfHasValue<string>. Por lo tanto se llama al método de extensión Add para agregar potencialmente el valor dentro de Maybe a la lista.
Ten en cuenta que podría haber definido el método Add para trabajar en Maybe<T> en lugar de AddIfHasValue<T>. El código en miMetodo se vería así en este caso:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void miMetodo() { Maybe<string> logMaybe = Maybe.Some("entry9"); var lista = new List<string>() { "entrada1", logMaybe, "entrada2" }; } |
El problema que puede suceder es que un Desarrollador que vea el código, espera que haya 3 elementos en la lista. Agregar ToAddIfHasValue facilita al Desarrollador comprender que el valor solo se agregará si existe.
Conclusión
Hemos visto algunos métodos que están diseñados para facilitar el manejo de type Maybes. Espero que estos métodos te sean útiles.
Nota(s)
- Los métodos y códigos expuestos en este Post, pueden quedar obsoletos, ser modificados o continuar, esto no depende de mi, si no de los Desarrolladores que dan soporte a C.
- No olvides que debemos usar la Tecnología para hacer cosas Buenas por el Mundo.
Síguenos en nuestras Redes Sociales para que no te pierdas nuestros próximos contenidos.
- C#
- 14-04-2020
- 16-04-2020
- Crear un Post - Eventos Devs - Foro
Social
Redes Sociales (Developers)
Redes Sociales (Digital)