Pensamiento funcional Parte 10

¿Te imaginas que esta es la décima parte del ciclo! Aunque la narración se había centrado anteriormente en un estilo puramente funcional, a veces es conveniente cambiar a un estilo orientado a objetos. Y una de las características clave de un estilo orientado a objetos es la capacidad de adjuntar funciones a una clase y acceder a la clase a través de un punto para obtener el comportamiento deseado.






En F #, esto es posible con una función llamada "extensiones de tipo". Cualquier tipo de F #, no solo una clase, puede tener funciones adjuntas.


Aquí hay un ejemplo de adjuntar una función a un tipo de registro.


module Person = type T = {First:string; Last:string} with // -,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} let person = Person.create "John" "Doe" let fullname = person.FullName 

Puntos clave a los que prestar atención:


  • La palabra clave with indica el comienzo de una lista de miembros.
  • La palabra clave member indica que la función es un miembro (es decir, un método)
  • La palabra this es la etiqueta del objeto en el que se llama este método (también llamado "autoidentificador"). Esta palabra es el prefijo del nombre de la función, y dentro de la función puede usarla para referirse a la instancia actual. No hay requisitos para las palabras utilizadas como autoidentificador, es suficiente que sean estables. Puede usar this , self o cualquier otra palabra que generalmente se usa como referencia para uno mismo.

No es necesario agregar un miembro junto con una declaración de tipo, siempre puede agregarlo más adelante en el mismo módulo:


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //  ,   type T with member this.SortableName = this.Last + ", " + this.First let person = Person.create "John" "Doe" let fullname = person.FullName let sortableName = person.SortableName 

Estos ejemplos demuestran la llamada a "extensiones intrínsecas". Se compilan en un tipo y estarán disponibles donde sea que se use el tipo. También se mostrarán al usar la reflexión.


Las extensiones internas incluso le permiten dividir una definición de tipo en varios archivos, siempre que todos los componentes usen el mismo espacio de nombres y se compilen en un solo ensamblaje. Al igual que con las clases parciales en C #, esto puede ser útil para separar el código generado y el escrito a mano.


Extensiones opcionales


Una alternativa es agregar un miembro adicional desde un módulo completamente diferente. Se llaman "extensiones opcionales". No se compilan dentro de la clase y requieren un módulo de alcance diferente para trabajar con ellos (este comportamiento se asemeja a los métodos de extensión de C #).


Por ejemplo, deje que se defina un tipo de Person :


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //   ,   type T with member this.SortableName = this.Last + ", " + this.First 

El siguiente ejemplo muestra cómo agregarle la extensión UppercaseName en otro módulo:


 //    module PersonExtensions = type Person.T with member this.UppercaseName = this.FullName.ToUpper() 

Ahora puedes probar esta extensión:


 let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

Vaya, tenemos un error. Sucedió porque PersonExtensions no PersonExtensions dentro del alcance. Como en C #, para usar cualquier extensión, debe ingresarlas en el ámbito.


Una vez que hagamos esto, funcionará:


 //    ! open PersonExtensions let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

Extensiones de tipo de sistema


También puede extender tipos desde bibliotecas .NET. Pero debe tenerse en cuenta que al expandir un tipo, debe usar su nombre real y no un alias.


Por ejemplo, si intenta expandir int , nada funcionará, porque int no int un nombre válido para el tipo:


 type int with member this.IsEven = this % 2 = 0 

En su lugar, use System.Int32 :


 type System.Int32 with member this.IsEven = this % 2 = 0 let i = 20 if i.IsEven then printfn "'%i' is even" i 

Miembros estáticos


Puede crear funciones miembro estáticas usando:


  • agregar static
  • eliminar this etiqueta

 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //   static member Create first last = {First=first; Last=last} let person = Person.T.Create "John" "Doe" let fullname = person.FullName 

Puede crear miembros estáticos para los tipos de sistema:


 type System.Int32 with static member IsOdd x = x % 2 = 1 type System.Double with static member Pi = 3.141 let result = System.Int32.IsOdd 20 let pi = System.Double.Pi 

Adjuntar características existentes


Un patrón muy común es la conexión de funciones independientes existentes a un tipo. Ofrece varias ventajas:


  • Durante el desarrollo, puede declarar funciones independientes que hacen referencia a otras funciones independientes. Esto simplificará el desarrollo, ya que la inferencia de tipos funciona mucho mejor con un estilo funcional que con uno orientado a objetos ("punto a punto").
  • Pero algunas funciones clave se pueden adjuntar a un tipo. Esto permite a los usuarios elegir cuál de los estilos usar: funcional u orientado a objetos.

Un ejemplo de tal solución es una función de la biblioteca F #, que calcula la longitud de la lista. Puede usar una función independiente del módulo List o llamarla como método de instancia.


 let list = [1..10] //   let len1 = List.length list // -  let len2 = list.Length 

En el siguiente ejemplo, el tipo inicialmente no tiene ningún miembro, luego se definen varias funciones y finalmente la función fullName se adjunta al tipo.


 module Person = // ,     type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last //       type T with member this.FullName = fullName this let person = Person.create "John" "Doe" let fullname = Person.fullName person //  let fullname2 = person.FullName //  

La función fullName tiene un parámetro, person . El miembro adjunto recibe el parámetro del autoenlace.


Agregar funciones existentes con múltiples parámetros


Hay otra buena característica. Si una función definida anteriormente toma varios parámetros, cuando la adjunte a un tipo, no tendrá que volver a enumerar todos estos parámetros. Es suficiente especificar primero this parámetro.


En el siguiente ejemplo, la función hasSameFirstAndLastName tiene tres parámetros. Sin embargo, al adjuntarlo es suficiente mencionar solo uno.


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let hasSameFirstAndLastName (person:T) otherFirst otherLast = person.First = otherFirst && person.Last = otherLast //      type T with member this.HasSameFirstAndLastName = hasSameFirstAndLastName this let person = Person.create "John" "Doe" let result1 = Person.hasSameFirstAndLastName person "bob" "smith" //  let result2 = person.HasSameFirstAndLastName "bob" "smith" //  

Por que funciona Sugerencia: ¡piense en curry y uso parcial!


Métodos de tupla


Cuando tenemos métodos con más de un parámetro, debe tomar una decisión:


  • podemos usar la forma estándar (currificada), donde los parámetros están separados por espacios, y se admite la aplicación parcial.
  • o podemos pasar todos los parámetros a la vez en forma de tupla separada por comas.

La forma curricular es más funcional, mientras que la forma de tupla está más orientada a objetos.


También se usa un formulario de tupla para interactuar con F # con bibliotecas .NET estándar, por lo que debe considerar este enfoque con más detalle.


Nuestro sitio de prueba será un tipo de Product con dos métodos, cada uno de los cuales se implementa mediante uno de los métodos descritos anteriormente. Los TupleTotal CurriedTotal y TupleTotal hacen lo mismo: calculan el costo total del producto para una cantidad y un descuento dados.


 type Product = {SKU:string; Price: float} with //   member this.CurriedTotal qty discount = (this.Price * float qty) - discount //   member this.TupleTotal(qty,discount) = (this.Price * float qty) - discount 

Código de prueba:


 let product = {SKU="ABC"; Price=2.0} let total1 = product.CurriedTotal 10 1.0 let total2 = product.TupleTotal(10,1.0) 

No hay mucha diferencia hasta ahora.


Pero sabemos que la versión curry se puede aplicar parcialmente:


 let totalFor10 = product.CurriedTotal 10 let discounts = [1.0..5.0] let totalForDifferentDiscounts = discounts |> List.map totalFor10 

Por otro lado, la versión de tupla es capaz de algo que no se puede curry, a saber:


  • Parámetros nombrados
  • Parámetros opcionales
  • Sobrecarga

Parámetros nombrados con parámetros en forma de tupla


El enfoque de tupla admite parámetros con nombre:


 let product = {SKU="ABC"; Price=2.0} let total3 = product.TupleTotal(qty=10,discount=1.0) let total4 = product.TupleTotal(discount=1.0, qty=10) 

Como puede ver, esto le permite cambiar el orden de los argumentos especificando explícitamente los nombres.


Atención: si solo algunos de los parámetros tienen nombres, estos parámetros siempre deben estar al final.


Parámetros opcionales con parámetros en forma de tupla


Para los métodos con parámetros en forma de tupla, puede marcar los parámetros como opcionales utilizando el prefijo en forma de un signo de interrogación delante del nombre del parámetro.


  • Si se establece el parámetro, se pasará Some value a la función
  • De lo contrario, None vendrá

Un ejemplo:


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty match discount with | None -> extPrice | Some discount -> extPrice - discount 

Y la prueba:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal2(10) //   let total2 = product.TupleTotal2(10,1.0) 

Comprobar explícitamente None and Some puede ser tedioso, pero hay una solución más elegante para manejar parámetros opcionales.


Hay una función defaultArg que toma un nombre de parámetro como primer argumento y un valor predeterminado como segundo. Si se establece el parámetro, se devolverá el valor correspondiente; de ​​lo contrario, el valor predeterminado.


El mismo código usando defaulArg :


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty let discount = defaultArg discount 0.0 extPrice - discount 

Método de sobrecarga


En C #, puede crear varios métodos con el mismo nombre que difieren en su firma (por ejemplo, varios tipos de parámetros y / o su número).


En un modelo puramente funcional, esto no tiene sentido: la función funciona con un tipo específico de argumento (dominio) y un tipo específico de valor de retorno (rango). La misma función no puede interactuar con otro dominio y rango.


Sin embargo, F # admite la sobrecarga de métodos, pero solo para los métodos (que se adjuntan a los tipos) y solo aquellos que están escritos en un estilo de tupla.


¡Aquí hay un ejemplo con otra variación del método TupleTotal !


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal3(qty) = printfn "using non-discount method" this.Price * float qty //   member this.TupleTotal3(qty, discount) = printfn "using discount method" (this.Price * float qty) - discount 

Como regla, el compilador de F # jura que hay dos métodos con el mismo nombre, pero en este caso esto es aceptable, porque se declaran en notación de tupla y sus firmas son diferentes. (Para dejar en claro qué método se llama, agregué pequeños mensajes para la depuración)


Ejemplo de uso:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal3(10) //   let total2 = product.TupleTotal3(10,1.0) 

Hey No tan rápido ... Las desventajas de usar métodos


Al provenir de un mundo orientado a objetos, puede tener la tentación de usar métodos en todas partes, porque es algo familiar. Pero debes tener cuidado, porque Tienen una serie de inconvenientes graves:


  • Los métodos no funcionan bien con la inferencia de tipos
  • Los métodos no funcionan bien con funciones de orden superior

De hecho, al abusar de los métodos, puede perderse los aspectos más potentes y útiles de la programación en F #.


Veamos a qué me refiero.


Los métodos interactúan mal con la inferencia de tipos


Volvamos al ejemplo con Person , en el que la misma lógica se implementó en una función independiente y en un método:


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last // - type T with member this.FullName = fullName this 

Ahora veamos qué tan bien funciona la inferencia de tipos con cada uno de los métodos. Supongamos que quiero imprimir el nombre completo de una persona, luego printFullName función printFullName , que toma a la person como parámetro.


Código usando una función independiente del módulo:


 open Person //    let printFullName person = printfn "Name is %s" (fullName person) //    // val printFullName : Person.T -> unit 

Se compila sin problemas y la inferencia de tipos identifica correctamente el parámetro como Person .


Ahora prueba la versión a través del punto:


 open Person //    " " let printFullName2 person = printfn "Name is %s" (person.FullName) 

Este código no se compila en absoluto, porque La inferencia de tipos no tiene suficiente información para determinar el tipo de parámetro. Cualquier objeto puede implementar .FullName : esto no es suficiente para la salida.


Sí, podemos anotar una función con un tipo de parámetro, pero debido a esto, se pierde todo el punto de inferencia de tipo automática.


Los métodos van mal con las funciones de orden superior


Un problema similar surge en las funciones de orden superior. Por ejemplo, hay una lista de personas y necesitamos obtener una lista de sus nombres completos.


En el caso de una función independiente, la solución es trivial:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //     list |> List.map fullName 

En el caso de un método de objeto, debe crear una lambda especial en todas partes:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //    list |> List.map (fun p -> p.FullName) 

Pero este sigue siendo un ejemplo bastante simple. Los métodos de los objetos son bastante susceptibles de composición, inconvenientes en la tubería, etc.


Por lo tanto, si eres nuevo en la programación funcional, te insto: si puedes, no uses métodos, especialmente en el proceso de aprendizaje. Serán una muleta que no le permitirá extraer el máximo beneficio de la programación funcional.


Recursos Adicionales


Hay muchos tutoriales para F #, incluidos los materiales para aquellos que vienen con experiencia en C # o Java. Los siguientes enlaces pueden ser útiles a medida que profundiza en F #:



También se describen varias otras formas de comenzar a aprender F # .


Finalmente, la comunidad F # es muy amigable para principiantes. Hay un chat muy activo en Slack, respaldado por la F # Software Foundation, con salas para principiantes a las que puedes unirte libremente . ¡Recomendamos encarecidamente que haga esto!


¡No te olvides de visitar el sitio de la comunidad de habla rusa F # ! Si tiene alguna pregunta sobre el aprendizaje de un idioma, estaremos encantados de discutirlo en las salas de chat:



Sobre autores de traducción


Traducido por @kleidemos
La traducción y los cambios editoriales fueron realizados por los esfuerzos de la comunidad de desarrolladores de F # de habla rusa . También agradecemos a @schvepsss y @shwars por preparar este artículo para su publicación.

Source: https://habr.com/ru/post/es433410/


All Articles