Pensamiento funcional Parte 9

¡Esto ya es parte 9 de una serie de artículos sobre programación funcional en F #! Estoy seguro de que en Habré no hay muchos ciclos tan largos. Pero no vamos a parar. Hoy hablaremos sobre funciones anidadas, módulos, espacios de nombres y tipos y funciones de mezcla en módulos.






Ahora sabes cómo definir funciones, pero ¿cómo organizarlas?


F # tiene tres opciones:


  • Las funciones se pueden anidar en otras funciones.
  • a nivel de aplicación, las funciones de nivel superior se agrupan en "módulos".
  • o puede seguir un enfoque orientado a objetos y adjuntar funciones a los tipos como métodos.

En este artículo, consideraremos los dos primeros métodos y el restante en el siguiente.


Funciones anidadas


En F #, puede definir funciones dentro de otras funciones. Esta es una buena manera de encapsular funciones auxiliares que solo son necesarias para la función principal y que no deberían ser visibles desde el exterior.


En el siguiente ejemplo, add anidado en addThreeNumbers :


 let addThreeNumbers xyz = //     let add n = fun x -> x + n //    x |> add y |> add z addThreeNumbers 2 3 4 

Las funciones anidadas pueden acceder a los parámetros principales directamente, porque están dentro de su alcance.
Entonces, en el ejemplo a continuación, la función anidada printError no necesita parámetros, porque ella puede acceder a n max directamente.


 let validateSize max n = //       let printError() = printfn "Oops: '%i' is bigger than max: '%i'" n max //    if n > max then printError() validateSize 10 9 validateSize 10 11 

Un patrón muy común es la función principal que define la función auxiliar recursiva anidada, que se llama con los valores iniciales correspondientes.
El siguiente es un ejemplo de dicho código:


 let sumNumbersUpTo max = //      let rec recursiveSum n sumSoFar = match n with | 0 -> sumSoFar | _ -> recursiveSum (n-1) (n+sumSoFar) //       recursiveSum max 0 sumNumbersUpTo 10 

Intente evitar el anidamiento profundo, especialmente en casos de acceso directo (no en forma de parámetros) a las variables principales.
Las funciones demasiado anidadas serán tan difíciles de entender como la peor de muchas ramas imperativas anidadas.


Un ejemplo de cómo no hacer:


 // wtf,    ? let fx = let f2 y = let f3 z = x * z let f4 z = let f5 z = y * z let f6 () = y * x f6() f4 y x * f2 x 

Módulos


Un módulo es simplemente una colección de funciones que se agrupan juntas, generalmente porque funcionan con el mismo tipo o tipos de datos.


Una definición de módulo es muy similar a una definición de función. Comienza con la palabra clave del module , luego viene el signo = , seguido del contenido del módulo.
El contenido del módulo debe formatearse con un desplazamiento, así como las expresiones en la definición de funciones.


Definición de un módulo que contiene dos funciones:


 module MathStuff = let add xy = x + y let subtract xy = x - y 

Si abre este código en Visual Studio, cuando pasa el cursor sobre add puede ver el nombre completo add , que en realidad es MathStuff.add , como si MastStuff fuera una clase, y add era un método.


De hecho, esto es exactamente lo que sucede. Detrás de escena, el compilador de F # crea una clase estática con métodos estáticos. El equivalente de C # se vería así:


 static class MathStuff { static public int add(int x, int y) { return x + y; } static public int subtract(int x, int y) { return x - y; } } 

Reconocer que los módulos son solo clases estáticas y las funciones son métodos estáticos proporcionará una buena comprensión de cómo funcionan los módulos en F #, ya que la mayoría de las reglas que se aplican a las clases estáticas también se aplican a los módulos.


Y al igual que en C #, cada función independiente debe ser parte de la clase, en F #, cada función independiente debe ser parte del módulo.


Acceso a funciones fuera del módulo.


Si necesita acceder a una función desde otro módulo, puede consultarla a través de su nombre completo.


 module MathStuff = let add xy = x + y let subtract xy = x - y module OtherStuff = //     MathStuff let add1 x = MathStuff.add x 1 

También puede importar todas las funciones de otro módulo usando la directiva open , después de lo cual puede usar el nombre corto en lugar del completo.


 module OtherStuff = open MathStuff //      let add1 x = add x 1 

Las reglas para usar nombres son bastante esperadas. Siempre puede acceder a una función por su nombre completo, o puede usar nombres relativos o incompletos dependiendo del alcance actual.


Módulos Anidados


Al igual que las clases estáticas, los módulos pueden contener módulos anidados:


 module MathStuff = let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Otros módulos pueden referirse a funciones en módulos anidados utilizando el nombre completo o relativo, según corresponda:


 module OtherStuff = open MathStuff let add1 x = add x 1 //   let add1Float x = MathStuff.FloatLib.add x 1.0 //   let sub1Float x = FloatLib.subtract x 1.0 

Módulos de nivel superior


Por lo tanto, dado que los módulos se pueden anidar, por lo tanto, subiendo la cadena, puede llegar a algún módulo principal del nivel superior. Realmente lo es


Los módulos de nivel superior se definen de manera diferente, a diferencia de los módulos que se mostraron anteriormente.


  • La module MyModuleName debe ser la primera declaración en el archivo
  • Signo = faltante
  • El contenido del módulo no debe sangrarse

En general, debe existir una declaración de "nivel superior" en cada archivo .FS origen. Hay algunas excepciones, pero sigue siendo una buena práctica. El nombre del módulo no tiene que coincidir con el nombre del archivo, pero dos archivos no pueden contener módulos con el mismo nombre.


Para archivos .FSX , la declaración del módulo no es necesaria, en este caso el nombre del archivo de script se convierte automáticamente en el nombre del módulo.


Un ejemplo de un MathStuff declarado como módulo "módulo superior":


 //    module MathStuff let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Tenga en cuenta que no hay sangría en el código de "nivel superior" ( module MathStuff ), mientras que el contenido de un módulo FloatLib anidado todavía tiene que FloatLib .


Otros contenidos del módulo


Además de las funciones, los módulos pueden contener otras declaraciones, como declaraciones de tipo, valores simples y código de inicialización (por ejemplo, constructores estáticos)


 module MathStuff = //  let add xy = x + y let subtract xy = x - y //   type Complex = {r:float; i:float} type IntegerFunction = int -> int -> int type DegreesOrRadians = Deg | Rad // "" let PI = 3.141 // "" let mutable TrigType = Deg //  /   do printfn "module initialized" 

Por cierto, si ejecuta estos ejemplos de forma interactiva, es posible que deba reiniciar la sesión con la frecuencia suficiente para que el código permanezca "actualizado" y no se infecte con cálculos anteriores.


Ocultamiento (superposición, sombreado)


Este es nuevamente nuestro módulo de muestra. Tenga en cuenta que MathStuff contiene la función de add , así como FloatLib .


 module MathStuff = let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

¿Qué sucede si abre ambos módulos en el alcance actual y llama a add ?


 open MathStuff open MathStuff.FloatLib let result = add 1 2 // Compiler error: This expression was expected to // have type float but here has type int 

Y sucedió que el módulo MathStuff.FloatLib redefinió el MathStuff original, que fue bloqueado (oculto) por el módulo FloatLib .


Como resultado, obtenemos el error del compilador FS0001, porque el primer parámetro 1 se esperaba como flotante. Para solucionar esto, debe cambiar 1 a 1.0 .


Desafortunadamente, en la práctica esto se pasa por alto discreta y fácilmente. A veces, utilizando esta técnica, puedes hacer trucos interesantes, casi como subclases, pero la mayoría de las veces la presencia de funciones del mismo nombre es molesta (por ejemplo, en el caso de la función de map extremadamente común).


Si desea evitar este comportamiento, hay una manera de detenerlo con el atributo RequireQualifiedAccess . El mismo ejemplo en el que ambos módulos están decorados con este atributo:


 [<RequireQualifiedAccess>] module MathStuff = let add xy = x + y let subtract xy = x - y //   [<RequireQualifiedAccess>] module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Ahora la directiva open no está disponible:


 open MathStuff //  open MathStuff.FloatLib //  

Pero aún puede acceder a las funciones (sin ninguna ambigüedad) a través de sus nombres completos:


 let result = MathStuff.add 1 2 let result = MathStuff.FloatLib.add 1.0 2.0 

Control de acceso


F # admite el uso de operadores de control de acceso estándar .NET como public , private e internal . El artículo de MSDN contiene información completa.


  • Estos especificadores de acceso se pueden aplicar a ("dejar ligar") funciones, valores, tipos y otras declaraciones de nivel superior en un módulo. También se pueden especificar para los propios módulos (por ejemplo, puede ser necesario un módulo anidado privado).
  • Por defecto, todo tiene acceso público (con la excepción de varios casos), por lo que para protegerlos necesitará usar private o internal .

Estos especificadores de acceso son solo una forma de controlar la visibilidad en F #. Una forma completamente diferente es utilizar archivos de firma que se asemejan a los archivos de encabezado C. Describen de manera abstracta el contenido del módulo. Las firmas son muy útiles para la encapsulación seria, pero para considerar sus capacidades, tendrá que esperar la serie planificada sobre encapsulación y seguridad basada en las capacidades .


Espacios de nombres


Los espacios de nombres en F # son similares a los espacios de nombres de C #. Se pueden usar para organizar módulos y tipos para evitar conflictos de nombres.


Un espacio de nombres declarado con la palabra clave de namespace :


 namespace Utilities module MathStuff = //  let add xy = x + y let subtract xy = x - y 

Debido a este espacio de nombres, el nombre completo del módulo MathStuff se MathStuff convertido en Utilities.MathStuff , y el nombre completo es Utilities.MathStuff.add .


Las mismas reglas de sangría se aplican a los módulos dentro de un espacio de nombres que se muestran arriba para los módulos.


También puede declarar un espacio de nombres explícitamente agregando un punto en el nombre del módulo. Es decir El código anterior se puede reescribir así:


 module Utilities.MathStuff //  let add xy = x + y let subtract xy = x - y 

El nombre completo del módulo MathStuff sigue siendo Utilities.MathStuff , pero ahora es un módulo de nivel superior y su contenido no necesita sangría.


Algunas características adicionales para usar espacios de nombres:


  • Los espacios de nombres son opcionales para los módulos. A diferencia de C #, para los proyectos de F # no hay un espacio de nombres predeterminado, por lo que un módulo de nivel superior sin un espacio de nombres será global. Si planea crear bibliotecas reutilizables, debe agregar varios espacios de nombres para evitar conflictos con el código de otras bibliotecas.
  • Los espacios de nombres pueden contener directamente declaraciones de tipo, pero no declaraciones de función. Como se señaló anteriormente, todas las declaraciones de funciones y valores deben ser parte de un módulo.
  • Finalmente, tenga en cuenta que los espacios de nombres no funcionan en los scripts. Por ejemplo, si intenta enviar una declaración de espacio de nombres, como namespace Utilities , a una ventana interactiva, se recibe un error.

Jerarquía de espacio de nombres


Puede crear una jerarquía de espacios de nombres simplemente dividiendo los nombres con puntos:


 namespace Core.Utilities module MathStuff = let add xy = x + y 

También puede declarar dos espacios de nombres en un archivo si lo desea. Debe tenerse en cuenta que todos los espacios de nombres deben declararse por su nombre completo; no admiten la anidación.


 namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core.Extra module MoreMathStuff = let add xy = x + y 

No es posible un conflicto de nombres entre el espacio de nombres y el módulo.


 namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core //    - Core.Utilities //     ! module Utilities = let add xy = x + y 

Mezcla de tipos y funciones en módulos


Como hemos visto, los módulos generalmente consisten en muchas funciones interdependientes que interactúan con un tipo de datos en particular.


En OOP, las estructuras de datos y las funciones por encima de ellas se combinarían en una clase. Y en F # funcional, las estructuras de datos y las funciones por encima de ellas se combinan en un módulo.


Hay dos patrones para combinar tipos y funciones:


  • tipo se declara por separado de las funciones
  • El tipo se declara en el mismo módulo que las funciones

En el primer caso, el tipo se declara fuera de cualquier módulo (pero en el espacio de nombres), después de lo cual las funciones que funcionan con este tipo se colocan en el módulo del mismo tipo.


 //    namespace Example //      type PersonType = {First:string; Last:string} //    ,     module Person = //  let create first last = {First=first; Last=last} // ,     let fullName {First=first; Last=last} = first + " " + last let person = Person.create "john" "doe" Person.fullName person |> printfn "Fullname=%s" 

Alternativamente, el tipo se declara dentro del módulo y tiene un nombre simple como " T " o el nombre del módulo. El acceso a las funciones es aproximadamente como sigue: MyModule.Func y MyModule.Func2 , y acceso al tipo: MyModule.T :


 module Customer = // Customer.T -      type T = {AccountId:int; Name:string} //  let create id name = {T.AccountId=id; T.Name=name} // ,     let isValid {T.AccountId=id; } = id > 0 let customer = Customer.create 42 "bob" Customer.isValid customer |> printfn "Is valid?=%b" 

Tenga en cuenta que en ambos casos debe haber una función de constructor que cree una nueva instancia del tipo (fábrica). Luego, en el código del cliente, apenas tiene que acceder al nombre del tipo explícitamente, y no tendrá que preguntarse si el tipo está dentro del módulo o no.


Entonces, ¿qué manera de elegir?


  • El primer enfoque es más parecido al .NET clásico, y debería preferirse si planea usar esta biblioteca para código fuera de F #, donde se espera una clase existente por separado.
  • El segundo enfoque es más común en otros lenguajes funcionales. El tipo dentro del módulo se compila como una clase anidada, que generalmente no es muy conveniente para los lenguajes OOP.

Por ti mismo, puedes experimentar con ambos métodos. En el caso del desarrollo del equipo, se debe elegir un estilo.


Módulos que contienen solo tipos


Si hay muchos tipos que deben declararse sin ninguna función, no se moleste en usar el módulo. Puede declarar tipos directamente en el espacio de nombres sin recurrir a clases anidadas.


Por ejemplo, es posible que desee hacer esto:


 //    module Example //     type PersonType = {First:string; Last:string} //    ,  ... 

Y aquí hay otra forma de hacer lo mismo. El module palabras simplemente se reemplaza con el namespace palabras.


 //    namespace Example //     type PersonType = {First:string; Last:string} 

En ambos casos, PersonType tendrá el mismo nombre completo.


Tenga en cuenta que este reemplazo solo funciona con tipos. Las funciones siempre deben declararse dentro del módulo.


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/es433406/


All Articles