ingles idiomáticas idiomaticas expresiones ejemplos f# idiomatic

f# - idiomáticas - expresiones idiomaticas pdf



¿Este uso de la opción es idiomático en F#? (6)

Tengo la siguiente función que verifica la existencia de un customer en una fuente de datos y devuelve la identificación. ¿Es esta la forma correcta / idiomática de usar el tipo de Option ?

let findCustomerId fname lname email = let (==) (a:string) (b:string) = a.ToLower() = b.ToLower() let validFName name (cus:customer) = name == cus.firstname let validLName name (cus:customer) = name == cus.lastname let validEmail email (cus:customer) = email == cus.email let allCustomers = Data.Customers() let tryFind pred = allCustomers |> Seq.tryFind pred tryFind (fun cus -> validFName fname cus && validEmail email cus && validLName lname cus) |> function | Some cus -> cus.id | None -> tryFind (fun cus -> validFName fname cus && validEmail email cus) |> function | Some cus -> cus.id | None -> tryFind (fun cus -> validEmail email cus) |> function | Some cus -> cus.id | None -> createGuest() |> fun cus -> cus.id


En primer lugar, esto puede no estar directamente relacionado con su pregunta, pero es posible que desee reordenar la lógica en esta función.

En lugar de:

"Busco un cliente que coincida con fname, lastname y emai; en su defecto, busco fname + email, luego solo correo electrónico, luego creo un invitado"

puede ser mejor proceder de esta manera:

"Busco un correo electrónico coincidente. Si obtengo varias coincidencias, busco un nombre fname correspondiente, y si hay múltiplos, busco un nombre que coincida" .

Esto no solo te permitiría estructurar mejor tu código, sino que te obligaría a lidiar con posibles problemas en la lógica.

Por ejemplo, ¿qué sucede si tiene varios correos electrónicos coincidentes, pero ninguno de ellos tiene el nombre correcto? Actualmente, simplemente elige el primero en la secuencia, que puede ser o no lo que desee, dependiendo de cómo se solicite Data.Customers (), si está ordenado.

Ahora, si los correos electrónicos deben ser únicos, entonces esto no será un problema, pero si ese fuera el caso, ¡también podría saltearse la verificación de los nombres / apellidos!

(Dudo mencionarlo, pero también podría acelerar un poco su código, ya que no revisa los registros innecesariamente más de una vez para los mismos campos, ni busca campos adicionales cuando solo basta con el correo electrónico).

Y al llegar a su pregunta ahora, el problema no está en el uso de Option , ¡el problema es que está realizando básicamente la misma operación tres veces! ("Buscar coincidencias, luego, si no las encuentra, busque una alternativa"). Refactorizar la función de forma recursiva eliminará la fea estructura diagonal y le permitirá extender la función trivialmente en el futuro para verificar si hay campos adicionales.

Algunas otras sugerencias menores para su código:

  • Como solo validFoo funciones validFoo ayuda de validFoo con los mismos argumentos para Foo , puedes hornearlas en las definiciones de función para adelgazar el código.
  • El uso de .toLower() / .toUpper() para la comparación de cadenas insensibles a mayúsculas y minúsculas es común, pero es un poco subóptimo, ya que en realidad crea nuevas copias en minúsculas de cada cadena. La forma correcta es usar String.Equals(a, b, StringComparison.CurrentCultureIgnoreCase) . El 99% de las veces se trata de una microoptimización irrelevante, pero si tienes una gran base de datos de clientes y haces muchas búsquedas de clientes, este es el tipo de función en la que realmente podría ser importante.
  • Si es posible, modificaría la función createGuest para que devuelva todo customer objeto del customer , y solo tome el .id como la última línea de esta función, o mejor aún, devuelva un customer de esta función y ofrezca un separar one-liner findCustomerId = findCustomer >> (fun c -> c.id) para facilitar su uso.

Con todo eso, tenemos lo siguiente. Por el bien del ejemplo, supondré que en el caso de varias coincidencias igualmente válidas, querrá la última , o la más reciente. Pero también podría lanzar una excepción, ordenar por un campo de fecha, o lo que sea.

let findCustomerId fname lname email = let (==) (a:string) (b:string) = String.Equals(a, b, StringComparison.CurrentCultureIgnoreCase) let validFName = fun (cus:customer) -> fname == cus.firstname let validLName = fun (cus:customer) -> lname == cus.lastname let validEmail = fun (cus:customer) -> email == cus.email let allCustomers = Data.Customers () let pickBetweenEquallyValid = Seq.last let rec check customers predicates fallback = match predicates with | [] -> fallback | pred :: otherPreds -> let matchingCustomers = customers |> Seq.filter pred match Seq.length matchingCustomers with | 0 -> fallback | 1 -> (Seq.head matchingCustomers).id | _ -> check matchingCustomers otherPreds (pickBetweenEquallyValid matchingCustomers).id check allCustomers [validEmail; validFName; validLName] (createGuest())

Una última cosa: esas expresiones feos (y a menudo O (n)) Seq.foo todas partes son necesarias porque no sé qué tipo de secuencia devuelve Data.Customers , y la clase general de Seq no es muy amigable con la coincidencia de patrones .

Si, por ejemplo, Data.Customers devuelve una matriz, la legibilidad se mejorará significativamente:

let pickBetweenEquallyValid results = results.[results.Length - 1] let rec check customers predicates fallback = match predicates with | [] -> fallback | pred :: otherPreds -> let matchingCustomers = customers |> Array.filter pred match matchingCustomers with | [||] -> fallback | [| uniqueMatch |] -> uniqueMatch.id | _ -> check matchingCustomers otherPreds (pickBetweenEquallyValid matchingCustomers).id check allCustomers [validEmail; validFName; validLName] (createGuest())


Hablando del uso idiomático del lenguaje, en primer lugar, F # promueve la escritura de un código que refleja claramente la intención. Cuando mira su fragmento desde este punto de vista, la mayor parte del código es excesivo y solo oculta la observación de que el valor devuelto no depende de ningún modo del nombre o lastname .

Su fragmento se puede refactorizar a una función equivalente mucho más corta y mucho más clara que:

  • recibir tres argumentos ignora todo, pero el email ,
  • luego de la secuencia de todos los clientes trata de encontrar uno teniendo (ignorando el caso) el mismo email ,
  • si tal encontrado, luego devuelve su id , createGuest().id devuelve createGuest().id

que casi se traduce literalmente en

let findCustomerId _ _ email = Data.Customers() |> Seq.tryFind (fun c -> System.String.Compare(email,c.email,true) = 0) |> function Some(c) -> c.id | None -> createGuest().id


Las opciones forman una mónada y también son monoidales, ya que admiten dos funciones de la forma

zero: Option<T> combine: Option<T> -> Option<T> -> Option<T>

las expresiones de cálculo se utilizan para proporcionar una forma más agradable de trabajar con mónadas y también son compatibles con las operaciones monoides. Por lo tanto, puede implementar un generador de cálculos para la Option :

type OptionBuilder() = member this.Return(x) = Some(x) member this.ReturnFrom(o: Option<_>) = o member this.Bind(o, f) = match o with | None -> None | Some(x) -> f x member this.Delay(f) = f() member this.Yield(x) = Some(x) member this.YieldFrom(o: Option<_>) = o member this.Zero() = None member this.Combine(x, y) = match x with | None -> y | _ -> x let maybe = OptionBuilder()

donde Combine devuelve el primer valor de Option no vacío. A continuación, puede usar esto para implementar su función:

let existing = maybe { yield! tryFind (fun cus -> validFName fname cus && validEmail email cus && validLName lname cus) yield! tryFind (fun cus -> validFName fname cus && validEmail email cus) yield! tryFind (fun cus -> validEmail email cus) } match existing with | Some(c) -> c.id | None -> (createGuest()).id


Permítanme reformular y corregir la declaración del problema:

Estoy buscando 1) coincidencia de nombre, apellido y correo electrónico en cuyo caso me gustaría finalizar la iteración. En su defecto, almaceno temporalmente a un cliente con 2) nombre y correo electrónico coincidentes o, menos preferiblemente, 3) solo un correo electrónico coincidente, y sigo buscando 1). Los elementos de la secuencia deben evaluarse a lo sumo una vez.

Este tipo de problema no es muy apropiado para las funciones Seq , ya que implica el estado en una jerarquía creciente, con terminación cuando se alcanza el estado más alto. Entonces, hagámoslo de manera imperativa, haciendo que el estado sea mutable, pero usando una unión discriminada para codificarlo y con una coincidencia de patrones para efectuar las transiciones de estado.

type MatchType<''a> = | AllFields of ''a | FNameEmail of ''a | Email of ''a | NoMatch let findCustomerId fname lname email = let allCustomers = Data.Customers () let (==) a b = // Needs tweaking to pass the Turkey Test System.String.Equals(a, b, System.StringComparison.CurrentCultureIgnoreCase) let notAllFields = function AllFields _ -> false | _ -> true let state = ref NoMatch use en = allCustomers.GetEnumerator() while notAllFields !state && en.MoveNext() do let cus = en.Current let fn = fname == cus.firstname let ln = lname == cus.lastname let em = email == cus.email match !state with | _ when fn && ln && em -> state := AllFields cus | Email _ | NoMatch when fn && em -> state := FNameEmail cus | NoMatch when em -> state := Email cus | _ -> () match !state with | AllFields cus | FNameEmail cus | Email cus -> cus.id | NoMatch -> createGuest().id


Una pequeña abstracción puede recorrer un largo camino en términos de legibilidad ...

let bindNone binder opt = if Option.isSome opt then opt else binder () let findCustomerId fname lname email = let allCustomers = Data.Customers () let (==) (a:string) (b:string) = a.ToLower () = b.ToLower () let validFName name (cus:customer) = name == cus.firstname let validLName name (cus:customer) = name == cus.lastname let validEmail email (cus:customer) = email == cus.email let tryFind pred = allCustomers |> Seq.tryFind pred tryFind (fun cus -> validFName fname cus && validEmail email cus && validLName lname cus) |> bindNone (fun () -> tryFind (fun cus -> validFName fname cus && validEmail email cus)) |> bindNone (fun () -> tryFind (fun cus -> validEmail email cus)) |> bindNone (fun () -> Some (createGuest ())) |> Option.get |> fun cus -> cus.id

Mucho más fácil de seguir, y la única sobrecarga es unos pocos controles null adicionales.

Además, si yo fuera usted, porque la mayoría de estas funciones son tan pequeñas / triviales, inline prudencia.


Nunca es bueno cuando tienes sangría al sangrar, por lo que valdría la pena ver qué puedes hacer al respecto.

Aquí hay una forma de abordar el problema, introduciendo una pequeña función auxiliar:

let tryFindNext pred = function | Some x -> Some x | None -> tryFind pred

Puede usarlo dentro de la función findCustomerId para aplanar las opciones de findCustomerId :

let findCustomerId'' fname lname email = let (==) (a:string) (b:string) = a.ToLower() = b.ToLower() let validFName name (cus:customer) = name == cus.firstname let validLName name (cus:customer) = name == cus.lastname let validEmail email (cus:customer) = email == cus.email let allCustomers = Data.Customers() let tryFind pred = allCustomers |> Seq.tryFind pred let tryFindNext pred = function | Some x -> Some x | None -> tryFind pred tryFind (fun cus -> validFName fname cus && validEmail email cus && validLName lname cus) |> tryFindNext (fun cus -> validFName fname cus && validEmail email cus) |> tryFindNext (fun cus -> validEmail email cus) |> function | Some cus -> cus.id | None -> createGuest().id

Esto es muy similar al enfoque descrito aquí .