|
| 1 | +=== Pattern Matching |
| 2 | + |
| 3 | +SScala tiene coincidencia de patrones (__pattern matching__) nativa, una de las ventajas sobre el Java __**puro**__. The basic syntax is close to Java'sLa sintaxis básica es similar al `switch` de Java: |
| 4 | + |
| 5 | +[source,java] |
| 6 | +---- |
| 7 | +val s = i match { |
| 8 | + case 1 => "one" |
| 9 | + case 2 => "two" |
| 10 | + case _ => "?" |
| 11 | +} |
| 12 | +---- |
| 13 | + |
| 14 | +Notablemente, __match__ es una expresión, lo que significa que produce un resultado. Además, ofrece: |
| 15 | + |
| 16 | +* Parámetros nombrados: ``case i: Int => "Int " + i`` |
| 17 | +* Desestructuración de objetos: ``case Some(i) => i`` |
| 18 | +* Condiciones de guarda (guards): ``case Some(i) if i > 0 => "positive " + i`` |
| 19 | +* Múltiples condiciones: ``case "-h" | "--help" => displayHelp`` |
| 20 | +* Verificaciones en tiempo de compilación para exhaustividad |
| 21 | +
|
| 22 | +El __**Pattern matching**__ es una gran característica que nos ahorra escribir cadenas de ramas `if-then-else`. Reduce la cantidad de código mientras se enfoca en las partes relevantes. |
| 23 | + |
| 24 | +==== Fundamentos de Match para Java |
| 25 | + |
| 26 | +Vavr proporciona una API de coincidencia (__match__) que es similar a la de Scala. Se habilita agregando el siguiente import a nuestra aplicación: |
| 27 | + |
| 28 | +[source,java] |
| 29 | +---- |
| 30 | +import static io.vavr.API.*; |
| 31 | +---- |
| 32 | + |
| 33 | +Teniendo los métodos estáticos __Match__, __Case__ y los __patrones atómicos (__atomic patterns__)__ |
| 34 | + |
| 35 | +* ``$()`` - patrón comodín (__wildcard__) |
| 36 | +* ``$(value)`` - patrón de igualdad |
| 37 | +* ``$(predicate)`` - patrón condicional |
| 38 | + |
| 39 | +En el alcance, el ejemplo inicial de Scala puede expresarse de esta manera: |
| 40 | + |
| 41 | +[source,java] |
| 42 | +---- |
| 43 | +String s = Match(i).of( |
| 44 | + Case($(1), "one"), |
| 45 | + Case($(2), "two"), |
| 46 | + Case($(), "?") |
| 47 | +); |
| 48 | +---- |
| 49 | + |
| 50 | +⚡ Usamos nombres de métodos en mayúsculas uniformes porque 'case' es una palabra clave en Java. Esto hace que la API sea especial. |
| 51 | + |
| 52 | +===== Exhaustividad (__Exhaustiveness__) |
| 53 | + |
| 54 | +El último patrón comodín (__wildcard__) ``$()`` nos protege de un `MatchError`, que se lanza si ningún caso coincide. |
| 55 | + |
| 56 | +Debido a que no podemos realizar verificaciones de exhaustividad como lo hace el compilador de Scala, ofrecemos la posibilidad de devolver un resultado opcional: |
| 57 | + |
| 58 | +[source,java] |
| 59 | +---- |
| 60 | +Option<String> s = Match(i).option( |
| 61 | + Case($(0), "zero") |
| 62 | +); |
| 63 | +---- |
| 64 | + |
| 65 | +===== Syntactic Sugar (__Azúcar Sintáctico__) |
| 66 | + |
| 67 | +Como se mostró anteriormente, ``Case`` permite coincidir con patrones condicionales. |
| 68 | + |
| 69 | +[source,java] |
| 70 | +---- |
| 71 | +Case($(predicate), ...) |
| 72 | +---- |
| 73 | + |
| 74 | +Vavr ofrece un conjunto de predicados predeterminados. |
| 75 | + |
| 76 | +[source,java] |
| 77 | +---- |
| 78 | +import static io.vavr.Predicates.*; |
| 79 | +---- |
| 80 | + |
| 81 | +Estos pueden usarse para expresar el ejemplo inicial de Scala de la siguiente manera: |
| 82 | + |
| 83 | +[source,java] |
| 84 | +---- |
| 85 | +String s = Match(i).of( |
| 86 | + Case($(is(1)), "one"), |
| 87 | + Case($(is(2)), "two"), |
| 88 | + Case($(), "?") |
| 89 | +); |
| 90 | +---- |
| 91 | + |
| 92 | +**Condiciones Múltiples** |
| 93 | + |
| 94 | +Usamos el predicado ``isIn`` para verificar múltiples condiciones: |
| 95 | + |
| 96 | +[source,java] |
| 97 | +---- |
| 98 | +Case($(isIn("-h", "--help")), ...) |
| 99 | +---- |
| 100 | + |
| 101 | +**Realizando Efectos Secundarios** |
| 102 | + |
| 103 | +`Match` actúa como una expresión y produce un valor. Para realizar efectos secundarios (__side-effects__), necesitamos usar la función auxiliar ``run`` que devuelve ``Void``: |
| 104 | + |
| 105 | +[source,java] |
| 106 | +---- |
| 107 | +Match(arg).of( |
| 108 | + Case($(isIn("-h", "--help")), o -> run(this::displayHelp)), |
| 109 | + Case($(isIn("-v", "--version")), o -> run(this::displayVersion)), |
| 110 | + Case($(), o -> run(() -> { |
| 111 | + throw new IllegalArgumentException(arg); |
| 112 | + })) |
| 113 | +); |
| 114 | +---- |
| 115 | + |
| 116 | +⚡ ``run`` se utiliza para evitar ambigüedades y porque ``void`` no es un valor de retorno válido en Java. |
| 117 | + |
| 118 | +*Precaución:* ``run`` no debe usarse como valor de retorno directo, es decir, fuera del cuerpo de una lambda: |
| 119 | + |
| 120 | +[source,java] |
| 121 | +---- |
| 122 | +// Incorrecto |
| 123 | +Case($(isIn("-h", "--help")), run(this::displayHelp)) |
| 124 | +---- |
| 125 | + |
| 126 | +De lo contrario, los `Case` se evaluarán de forma anticipada __antes__ de que los patrones sean comparados, lo que rompe toda la expresión `Match`. En su lugar, se debe usar dentro del cuerpo de una lambda: |
| 127 | + |
| 128 | +[source,java] |
| 129 | +---- |
| 130 | +// Correcto |
| 131 | +Case($(isIn("-h", "--help")), o -> run(this::displayHelp)) |
| 132 | +---- |
| 133 | + |
| 134 | +Como podemos ver, ``run`` es propenso a errores si no se usa correctamente. Ten cuidado. Estamos considerando marcarlo como obsoleto en una versión futura y, tal vez, proporcionar una mejor API para realizar efectos secundarios. |
| 135 | + |
| 136 | +===== Parámetros Nombrados |
| 137 | + |
| 138 | +Vavr aprovecha las lambdas para proporcionar parámetros nombrados para los valores coincidentes. |
| 139 | + |
| 140 | +[source,java] |
| 141 | +---- |
| 142 | +Number plusOne = Match(obj).of( |
| 143 | + Case($(instanceOf(Integer.class)), i -> i + 1), |
| 144 | + Case($(instanceOf(Double.class)), d -> d + 1), |
| 145 | + Case($(), o -> { throw new NumberFormatException(); }) |
| 146 | +); |
| 147 | +---- |
| 148 | + |
| 149 | +Hasta ahora, hemos coincidido valores directamente utilizando patrones atómicos. Si un patrón atómico coincide, el tipo correcto del objeto coincidente se infiere del contexto del patrón. |
| 150 | + |
| 151 | +A continuación, exploraremos patrones recursivos que pueden coincidir con gráficos de objetos de profundidad (teóricamente) arbitraria. |
| 152 | + |
| 153 | +===== Descomposición de Objetos |
| 154 | + |
| 155 | +En Java usamos constructores para instanciar clases. Entendemos la __descomposición de objetos__ como la destrucción de objetos en sus partes. |
| 156 | + |
| 157 | +Mientras que un constructor es una __función__ que se __aplica__ a los argumentos y devuelve una nueva instancia, un deconstructor es una función que toma una instancia y devuelve sus partes. Decimos que un objeto está __descompuesto__. |
| 158 | + |
| 159 | +La destrucción de objetos no necesariamente es una operación única. Por ejemplo, un `LocalDate` puede descomponerse en: |
| 160 | + |
| 161 | +* Los componentes de año, mes y día. |
| 162 | +* El valor `long` que representa los milisegundos desde la época de un `Instant` correspondiente. |
| 163 | +* etc. |
| 164 | + |
| 165 | +==== Patrones |
| 166 | + |
| 167 | +En Vavr usamos patrones para definir cómo se deconstruye una instancia de un tipo específico. Estos patrones pueden usarse junto con la API de coincidencia (`Match`). |
| 168 | + |
| 169 | +===== Patrones Predefinidos |
| 170 | + |
| 171 | +Para muchos tipos de Vavr ya existen patrones de coincidencia predefinidos. Estos se importan mediante |
| 172 | + |
| 173 | +[source,java] |
| 174 | +---- |
| 175 | +import static io.vavr.Patterns.*; |
| 176 | +---- |
| 177 | + |
| 178 | +Por ejemplo, ahora podemos coincidir con el resultado de un `Try`: |
| 179 | + |
| 180 | +[source,java] |
| 181 | +---- |
| 182 | +Match(_try).of( |
| 183 | + Case($Success($()), value -> ...), |
| 184 | + Case($Failure($()), x -> ...) |
| 185 | +); |
| 186 | +---- |
| 187 | + |
| 188 | +⚡ Un primer prototipo de la API de coincidencia (`Match`) de Vavr permitía extraer una selección definida por el usuario de objetos a partir de un patrón de coincidencia. Sin el soporte adecuado del compilador, esto no es práctico porque el número de métodos generados crecía exponencialmente. La API actual hace un compromiso: todos los patrones se coinciden, pero solo los patrones raíz son __descompuestos__. |
| 189 | + |
| 190 | +[source,java] |
| 191 | +---- |
| 192 | +Match(_try).of( |
| 193 | + Case($Success($Tuple2($("a"), $())), tuple2 -> ...), |
| 194 | + Case($Failure($(instanceOf(Error.class))), error -> ...) |
| 195 | +); |
| 196 | +---- |
| 197 | + |
| 198 | +Aquí los patrones raíz son `Success` y `Failure`. Estos se descomponen en `Tuple2` y `Error`, teniendo los tipos genéricos correctos. |
| 199 | + |
| 200 | +⚡ Los tipos profundamente anidados se infieren según el argumento de `Match` y __**not**__ según los patrones coincidentes. |
| 201 | + |
| 202 | +===== Patrones Definidos por el Usuario |
| 203 | + |
| 204 | +Es esencial poder descomponer objetos arbitrarios, incluidas las instancias de clases finales. Vavr hace esto de forma declarativa al proporcionar las anotaciones en tiempo de compilación ``@Patterns`` y ``@Unapply``. |
| 205 | + |
| 206 | +Para habilitar el procesador de anotaciones, el artefacto http://search.maven.org/#search%7Cga%7C1%7Cvavr-match[vavr-match] debe añadirse como dependencia del proyecto. |
| 207 | + |
| 208 | +⚡ Nota: Por supuesto, los patrones pueden implementarse directamente sin usar el generador de código. Para más información, consulta el código fuente generado. |
| 209 | + |
| 210 | +[source,java] |
| 211 | +---- |
| 212 | +import io.vavr.match.annotation.*; |
| 213 | +
|
| 214 | +@Patterns |
| 215 | +class My { |
| 216 | +
|
| 217 | + @Unapply |
| 218 | + static <T> Tuple1<T> Optional(java.util.Optional<T> optional) { |
| 219 | + return Tuple.of(optional.orElse(null)); |
| 220 | + } |
| 221 | +} |
| 222 | +---- |
| 223 | + |
| 224 | +El procesador de anotaciones coloca un archivo `MyPatterns` en el mismo paquete (por defecto en `target/generated-sources`). También se admiten clases internas. Caso especial: si el nombre de la clase es `$`, el nombre de la clase generada será simplemente `Patterns`, sin prefijo. |
| 225 | + |
| 226 | +===== Guardas (__Guards__) |
| 227 | + |
| 228 | +Ahora podemos coincidir con objetos `Optionals` utilizando __guards__. |
| 229 | + |
| 230 | +[source,java] |
| 231 | +---- |
| 232 | +Match(optional).of( |
| 233 | + Case($Optional($(v -> v != null)), "defined"), |
| 234 | + Case($Optional($(v -> v == null)), "empty") |
| 235 | +); |
| 236 | +---- |
| 237 | + |
| 238 | +Los predicados podrían simplificarse implementando ``isNull`` y ``isNotNull``. |
| 239 | + |
| 240 | +⚡ ¡Y sí, extraer un `null` es extraño! En lugar de usar el `Optional` de Java, prueba con la `Option` de Vavr. |
| 241 | + |
| 242 | +[source,java] |
| 243 | +---- |
| 244 | +Match(option).of( |
| 245 | + Case($Some($()), "defined"), |
| 246 | + Case($None(), "empty") |
| 247 | +); |
| 248 | +---- |
0 commit comments