Patrones activos en F#

- 10 minutos de lectura

En una entrada anterior vimos c贸mo con pattern matching podemos hacer nuestro c贸digo m谩s expresivo cuando queremos comparar datos con estructuras l贸gicas. Cada patr贸n nos permite comparar los valores de entrada, descomponerlos y combinarlos con una estructura de datos. Sin embargo, con pattern matching los patrones soportados est谩n limitados a unos tipos determinados, como listas, tuplas o matrices y valores constantes de tipo cadena, num茅rico, enumeraci贸n, etc.

En esta entrada veremos que con el uso de los patrones activos aumentaremos la potencia de pattern matching cuando los tipos de patrones incorporados no nos sean 煤tiles para nuestro prop贸sito, a la vez que a帽adiremos claridad a nuestro c贸digo, ya que podremos simplificar las construcciones eliminando el uso de las protecciones when.

Los patrones activos son un tipo especial de definici贸n de funci贸n llamada reconocedor activo (active recognizer), donde definimos los casos que podemos utilizar en las expresiones de pattern matching. Los nombres de los casos est谩n delimitados por los s铆mbolos (| y |), llamados delimitadores de patrones activos o banana clips y cada caso est谩 separado por un separador vertical.

La sintaxis de la definici贸n de patrones activos es la siguiente:

let (|identificador1|identificador2|...|) [ argumentos ] = expresin

Las funciones de definici贸n de un patr贸n activo deben incluir como m铆nimo un par谩metro de entrada que debe ser el valor con el que se buscar谩 coincidencias, pero adem谩s, como estas funciones son funciones parcializadas o curried, este valor deber ser el 煤ltimo de los argumentos. La funci贸n, adem谩s, debe devolver uno de los casos con nombre.

Podemos definir patrones activos de varias formas, seg煤n si definimos uno o varios casos, incluimos un comod铆n o utilizamos m谩s de un argumento.

Vamos a ver a continuaci贸n el primer ejemplo en el que simplificaremos la sintaxis mediante los patrones activos de uno de los ejemplos que utilic茅 en el post de pattern matching. Se trata del siguiente ejemplo, con el que implementamos la Kata FizzBuzz, donde utiliz谩bamos la protecci贸n when para establecer condiciones adicionales en cada uno de los casos:

let fizzbuzz x =
    function
    | x when x % 5 = 0 && x % 3 = 0 -> "FizzBuzz"
    | x when x % 3 = 0 -> "Fizz"
    | x when x % 5 = 0 -> "Buzz"
    | x -> string x

Para simplificar la funci贸n, podemos crear un reconocedor activo de la siguiente forma:

let (|Fizz|Buzz|FizzBuzz|Same|) x = 
    if x % 5 = 0 && x % 3 = 0 then FizzBuzz 
    elif x % 3 = 0 then Fizz 
    elif x % 5 = 0 then Buzz
    else Same x

En el que definimos cuatro casos con nombre (Fizz, Buzz, FizzBuzz y Same) y devolvemos el caso correcto seg煤n las mismas condiciones que utiliz谩bamos anteriormente. En este caso he utilizado la construcci贸n if..elif..else, pero nada nos impedir铆a utilizar tambi茅n otra expresi贸n match..with.

En cualquiera de los dos casos, ahora podemos escribir funciones pattern matching de la siguiente forma, en la que utilizamos casos definidos en la funci贸n del reconocedor activo en lugar de los patrones predefinidos con las protecciones.

let fizzBuzz =
    function
    | Fizz -> "Fizz"
    | Buzz -> "Buzz"
    | FizzBuzz -> "FizzBuzz"
    | Same x -> string x

seq {1..100}
|> Seq.map fizzBuzz

En el siguiente ejemplo vamos a definir un patr贸n activo que intentar谩 parsear el valor de una cadena a entero, booleano y num茅rico flotante y devolver谩 el caso que haya tenido 茅xito si se ha podido realizar la conversi贸n o por el contrario devolver谩 la cadena de entrada.

open System

let (|Int32|Float|Boolean|String|) input =
    let sucess, res = Int32.TryParse input
    if sucess then Int32 res
    else 
        let sucess, res = Double.TryParse input
        if sucess then Float res
        else
            let sucess, res = Boolean.TryParse input
            if sucess then Boolean res
            else String input

let describeType input =
    match input with
    | Int32 i -> sprintf "Integer: %i" i
    | Boolean b -> sprintf "Boolean: %b" b
    | Float f -> sprintf "Floating point: %f" f
    | String s -> sprintf "String: %s" s


["1"; "True"; "2.5"; "Text"]
|> List.map describeType

La diferencia en este ejemplo es que el valor devuelto es el caso con nombre (Int32, Float, Boolean o String) seguido de un valor que podemos utilizar en la expresi贸n de la funci贸n de pattern matching.

Patrones activos parciales

Los patrones activos est谩n limitados a siete casos con nombre. En el caso de que definamos una funci贸n con m谩s de siete casos, obtendremos un error de compilaci贸n. Si nos encontremos con un escenario en el que tenemos que utilizar m谩s de siete casos o necesitamos realizar un mapeo con cada posible entrada, tendremos que hacer uso de los patrones activos parciales.

La definici贸n de los patrones activos parciales tiene la misma sintaxis que los completos, pero en lugar de una lista de casos con nombre, tenemos que incluir un solo caso seguido del car谩cter de comod铆n _.

let (|identificador1|_|) [parmetros] = expresin

Otra diferencia a帽adida es que el valor devuelto por un patr贸n activo parcial no es el mismo de uno completo. En lugar de devolver el caso directamente, los patrones parciales devuelven un tipo opci贸n del tipo de patr贸n.

En el siguiente ejemplo creamos una funci贸n de pattern matching que nos indica si un n煤mero es divisible por 3 y por 5. En este caso estamos haciendo uso de un patr贸n activo parcial en el que solo definimos un caso, si el numero cumple las 2 condiciones la funci贸n devuelve Some(x) y en caso contrario devuelve None.

let (|DivisibleByThreeAndFive|_|) x = 
    if x % 3 = 0 && x % 5 = 0 then Some(x) else None

let describeNumber x =
    match x with
    | DivisibleByThreeAndFive x -> "Divisible by 3 and 5"
    | _ -> string x

seq {1..100}
|> Seq.map describeNumber

Patrones activos parametrizados

En todos los ejemplos que hemos visto hasta ahora solo hemos definido funciones con un par谩metro. Los 煤ltimos ejemplos que vamos a ver son la definici贸n de funciones reconocedoras que aceptan varios argumentos de entrada. Este tipo de patrones activos son conocidos como patrones activos parametrizados.

Es importante recordar que el 煤ltimo argumento ser谩 siempre el valor con el que queremos realizar la comparaci贸n de coincidencia.

En este primer ejemplo creamos un funci贸n reconocedora que acepta el par谩metro divisor adem谩s del valor a comparar y devuelve una tupla con el valor de entrada y el resto de la divisi贸n.

let (|DivisibleBy|_|) divisor n =
    Some (n, n % divisor)

En el caso de que el segundo valor de la tupla sea 0 indicar谩 que el n煤mero de entrada es divisible por el divisor. De esta forma podemos crear una funci贸n de pattern matching que nos devuelva el divisor de la siguiente forma:

let printDivisor n =
    match n with
    | DivisibleBy 2 (n,0) -> 2
    | DivisibleBy 3 (n,0) -> 3
    | _ -> n

[1..100]
|> List.map printDivisor

Y como 煤ltimo ejemplo, vamos a definir un patr贸n activo parcial que nos indicar谩 si una expresi贸n regular coincide con el valor de entrada.

open System.Text.RegularExpressions

let (|Regex|_|) regexPattern input =
    let regex = new Regex(regexPattern)
    let regexMatch = regex.Match(input)
    
    if regexMatch.Success then
        Some regexMatch.Value
    else
        None

Ahora podemos crear una funci贸n de pattern matching en la que podemos pasar como argumento la expresi贸n regular que queremos comprobar.

let describeContactType input =
    match input with
    | Regex  @"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" s -> sprintf "Email: %s" s
    | Regex  @"^\d{9}$" s -> sprintf "Phone: %s" s
    | _ -> sprintf "Other: %s" input

["alex@mail.com";"Address Sample";"650018066"]
|> List.map describeContactType

El resultado ser谩 el siguiente:

val it : string list =
    ["Email: alex@mail.com"; "Other: Address Sample"; "Phone: 611101010"]

Resumen

Con el uso de los patrones activos completamos toda la potencia que nos ofrece pattern matching en los casos en los que los patrones predefinidos no nos ofrecen la flexibilidad necesaria, adem谩s de mejorar la legibilidad del c贸digo.

Seg煤n el n煤mero de casos que definamos en el reconocedor activo tenemos cuatro variedades de patrones activos: de un solo caso, multi-caso, parciales y parametrizados. Los patrones de un solo caso se utilizan habitualmente para descomponer la entrada de una forma determinada. Los patrones parciales son aquellos en los que solo coincide una parte del valor de entrada. Y por 煤ltimo, los parametrizados realizan la misma funci贸n que los parciales pero admiten par谩metros adicionales para que puedan ser reutilizados f谩cilmente.

Referencias

Modelos activos (F#)