Pensamiento funcional Parte 7

Continuamos nuestra serie de artículos sobre programación funcional en F #. Hoy tenemos un tema muy interesante: la definición de funciones. Incluyendo, hablemos de funciones anónimas, funciones sin parámetros, funciones recursivas, combinadores y mucho más. ¡Mira debajo del gato!




Definición de función


Ya sabemos cómo crear funciones regulares utilizando la sintaxis "let":


let add xy = x + y 

En este artículo, veremos algunas otras formas de crear funciones, así como consejos para definirlas.


Funciones anónimas (lambdas)


Si está familiarizado con lambdas en otros idiomas, los siguientes párrafos le parecerán familiares. Las funciones anónimas (o "expresiones lambda") se definen de la siguiente manera:


 fun parameter1 parameter2 etc -> expression 

En comparación con las lambdas de C #, hay dos diferencias:


  • lambdas debe comenzar con la palabra clave fun , que no se requiere en C #
  • -> usa una sola flecha -> , en lugar de doble => de C #.

Definición lambda de la función de suma:


 let add = fun xy -> x + y 

Misma función en forma tradicional:


 let add xy = x + y 

Las lambdas se usan a menudo en forma de pequeñas expresiones o cuando no se desea definir una función separada para una expresión. Como ya has visto, cuando trabajas con listas, esto no es raro.


 //    let add1 i = i + 1 [1..10] |> List.map add1 //        [1..10] |> List.map (fun i -> i + 1) 

Tenga en cuenta que los paréntesis deben usarse alrededor de lambdas.


Las lambdas también se usan cuando se necesita una función claramente diferente. Por ejemplo, el " adderGenerator " discutido anteriormente, que discutimos anteriormente, puede reescribirse usando lambdas.


 //   let adderGenerator x = (+) x //     let adderGenerator x = fun y -> x + y 

La versión lambda es un poco más larga, pero inmediatamente deja en claro que se devolverá una función intermedia.


Las lambdas pueden estar anidadas. Otro ejemplo de una definición de adderGenerator , esta vez solo en lambdas.


 let adderGenerator = fun x -> (fun y -> x + y) 

¿Está claro que las tres definiciones son equivalentes?


 let adderGenerator1 xy = x + y let adderGenerator2 x = fun y -> x + y let adderGenerator3 = fun x -> (fun y -> x + y) 

De lo contrario, vuelva a leer el capítulo sobre curry . ¡Esto es muy importante para entender!


Coincidencia de patrones


Cuando se define una función, es posible pasarle parámetros explícitamente, como en los ejemplos anteriores, pero también es posible compararla directamente con una plantilla en la sección de parámetros. En otras palabras, la sección de parámetros puede contener patrones (patrones coincidentes), ¡y no solo identificadores!


El siguiente ejemplo demuestra el uso de patrones en una definición de función:


 type Name = {first:string; last:string} //    let bob = {first="bob"; last="smith"} //   //     let f1 name = //   let {first=f; last=l} = name //     printfn "first=%s; last=%s" fl //   let f2 {first=f; last=l} = //        printfn "first=%s; last=%s" fl //  f1 bob f2 bob 

Este tipo de comparación solo puede ocurrir cuando la correspondencia siempre es decidible. Por ejemplo, no puede hacer coincidir los tipos de unión y las listas de esta manera, porque algunos casos no pueden coincidir.


 let f3 (x::xs) = //       printfn "first element is=%A" x 

El compilador dará una advertencia sobre una coincidencia incompleta (una lista vacía causará un error en tiempo de ejecución en la entrada de esta función).


Error común: tuplas vs. muchos parámetros


Si proviene de un lenguaje tipo C, la tupla utilizada como único argumento de la función puede parecerse dolorosamente a una función de parámetros múltiples. ¡Pero esto no es lo mismo! Como señalé anteriormente, si ve una coma, es muy probable que sea una tupla. Los parámetros están separados por espacios.


Ejemplo de confusión:


 //      let addTwoParams xy = x + y //      -  let addTuple aTuple = let (x,y) = aTuple x + y //         //        let addConfusingTuple (x,y) = x + y 

  • La primera definición, " addTwoParams ", toma dos parámetros, separados por un espacio.
  • La segunda definición, " addTuple ", toma un parámetro. Este parámetro une "x" e "y" de la tupla y los suma.
  • La tercera definición, " addConfusingTuple ", toma un parámetro como " addTuple ", pero el truco es que esta tupla se desempaqueta (coincide con el patrón) y se enlaza como parte de la definición del parámetro mediante la coincidencia de patrones. Detrás de escena, todo sucede exactamente igual que en addTuple .

Echemos un vistazo a las firmas (siempre mírelas si no está seguro de algo).


 val addTwoParams : int -> int -> int //   val addTuple : int * int -> int // tuple->int val addConfusingTuple : int * int -> int // tuple->int 

Y ahora aquí:


 // addTwoParams 1 2 // ok --      addTwoParams (1,2) // error -     // => error FS0001: This expression was expected to have type // int but here has type 'a * 'b 

Aquí vemos un error en la segunda llamada.


Primero, el compilador trata (1,2) como una tupla generalizada del formulario ('a * 'b) , que intenta pasar como el primer parámetro a " addTwoParams ". Después de lo cual se queja de que el primer parámetro esperado addTwoParams no addTwoParams int , sino que se intentó pasar una tupla.


Para hacer una tupla, usa una coma.


 addTuple (1,2) // ok addConfusingTuple (1,2) // ok let x = (1,2) addTuple x // ok let y = 1,2 //  , //  ! addTuple y // ok addConfusingTuple y // ok 

Y viceversa, si pasa varios argumentos a una función que espera una tupla, también obtiene un error incomprensible.


 addConfusingTuple 1 2 // error --          // => error FS0003: This value is not a function and // cannot be applied 

Esta vez, el compilador decidió que una vez que se addConfusingTuple dos argumentos, addConfusingTuple debería curry. Y la entrada " addConfusingTuple 1 " es una aplicación parcial y debería devolver una función intermedia. Intentar llamar a esta función intermedia con el parámetro "2" arrojará un error, porque ¡no hay una función intermedia! Vemos el mismo error que en el capítulo sobre curry, donde discutimos problemas con demasiados parámetros.


¿Por qué no usar tuplas como parámetros?


La discusión de las tuplas anterior muestra otra forma de definir funciones con muchos parámetros: en lugar de pasarlas por separado, todos los parámetros pueden ensamblarse en una estructura. En el siguiente ejemplo, la función toma un solo parámetro: una tupla de tres elementos.


 let f (x,y,z) = x + y * z //  - int * int * int -> int //  f (1,2,3) 

Cabe señalar que la firma es diferente de la firma de una función con tres parámetros. Solo hay una flecha, un parámetro y asteriscos que apuntan a la tupla (int*int*int) .


¿Cuándo es necesario presentar argumentos con parámetros separados y cuándo una tupla?


  • Cuando las tuplas son significativas en sí mismas. Por ejemplo, para operaciones en espacio tridimensional, las tuplas triples serán más convenientes que las tres coordenadas por separado.
  • A veces, las tuplas se usan para combinar datos que deben almacenarse juntos en una sola estructura. Por ejemplo, los métodos TryParse de la biblioteca .NET devuelven el resultado y una variable booleana como una tupla. Pero para almacenar una gran cantidad de datos relacionados, es mejor definir una clase o registro ( registro .

Caso especial: tuplas y funciones de la biblioteca .NET


¡Al llamar a las bibliotecas .NET, las comas son muy comunes!


Todos aceptan tuplas y las llamadas tienen el mismo aspecto que en C #:


 //  System.String.Compare("a","b") //   System.String.Compare "a" "b" 

La razón es que las funciones de .NET clásico no son curry y no se pueden aplicar parcialmente. Todos los parámetros siempre deben transmitirse de inmediato, y la forma más obvia es usar una tupla.


Tenga en cuenta que estas llamadas solo parecen transferir tuplas, pero este es realmente un caso especial. No puedes pasar tuplas reales a tales funciones:


 let tuple = ("a","b") System.String.Compare tuple // error System.String.Compare "a","b" // error 

Si desea aplicar parcialmente las funciones .NET, simplemente escriba envoltorios sobre ellas, como se hizo anteriormente , o como se muestra a continuación:


 //    let strCompare xy = System.String.Compare(x,y) //    let strCompareWithB = strCompare "B" //      ["A";"B";"C"] |> List.map strCompareWithB 

Guía para seleccionar parámetros individuales y agrupados


La discusión de las tuplas conduce a un tema más general: ¿cuándo deben separarse los parámetros y cuándo deben agruparse?


Debe prestar atención a cómo F # difiere de C # a este respecto. En C #, todos los parámetros siempre se pasan, por lo que esta pregunta ni siquiera surge allí. En F #, debido a la aplicación parcial, solo se pueden representar algunos de los parámetros, por lo que es necesario distinguir entre el caso cuando los parámetros deben combinarse y el caso cuando son independientes.


Recomendaciones generales sobre cómo estructurar parámetros al diseñar sus propias funciones.


  • En el caso general, siempre es mejor usar parámetros separados en lugar de pasar una estructura, ya sea una tupla o un registro. Esto permite un comportamiento más flexible, como la aplicación parcial.
  • Pero, cuando se necesita pasar un grupo de parámetros a la vez, se debe utilizar algún tipo de mecanismo de agrupación.

En otras palabras, cuando desarrolle una función, pregúntese: "¿Puedo proporcionar este parámetro por separado?" Si la respuesta es no, entonces los parámetros deben agruparse.


Veamos algunos ejemplos:


 //     . //      ,       let add xy = x + y //         //      ,    let locateOnMap (xCoord,yCoord) = //  //      //      -     type CustomerName = {First:string; Last:string} let setCustomerName aCustomerName = //  let setCustomerName first last = //   //     //     //    ,     let setCustomerName myCredentials aName = // 

Finalmente, asegúrese de que el orden de los parámetros ayudará en la aplicación parcial (consulte el manual aquí ). Por ejemplo, ¿por qué puse myCredentials antes de aName en la última función?


Funciones sin parámetros.


A veces puede necesitar una función que no acepte ningún parámetro. Por ejemplo, necesita la función "hola mundo" que se puede llamar varias veces. Como se muestra en la sección anterior, la definición ingenua no funciona.


 let sayHello = printfn "Hello World!" //      

Pero esto se puede solucionar agregando un parámetro de unidad a la función o usando una lambda.


 let sayHello() = printfn "Hello World!" //  let sayHello = fun () -> printfn "Hello World!" //  

Después de eso, la función siempre debe llamarse con el argumento de la unit :


 //  sayHello() 

Lo que sucede con bastante frecuencia cuando interactúa con las bibliotecas .NET:


 Console.ReadLine() System.Environment.GetCommandLineArgs() System.IO.Directory.GetCurrentDirectory() 

Recuerda, ¡llámalos con los parámetros de la unit !


Definiendo nuevos operadores


Puede definir funciones utilizando uno o más caracteres de operador (consulte la documentación para obtener una lista de caracteres):


 //  let (.*%) xy = x + y + 1 

Debe usar paréntesis alrededor de los caracteres para definir funciones.


Los operadores que comienzan con * requieren un espacio entre paréntesis y * , porque en F # (* actúa como el comienzo de un comentario (como /*...*/ en C #):


 let ( *+* ) xy = x + y + 1 

Una vez definida, una nueva función se puede usar de la manera habitual si está entre paréntesis:


 let result = (.*%) 2 3 

Si la función se usa con dos parámetros, puede usar el registro de operador infijo sin paréntesis.


 let result = 2 .*% 3 

¡También puede definir operadores de prefijo comenzando por ! o ~ (con algunas restricciones, consulte la documentación )


 let (~%%) (s:string) = s.ToCharArray() // let result = %% "hello" 

En F #, la definición de declaraciones es una operación bastante común, y muchas bibliotecas exportarán declaraciones con nombres como >=> y <*> .


Estilo sin puntos


Ya hemos visto muchos ejemplos de funciones que carecían de los últimos parámetros para reducir el nivel de caos. Este estilo se llama estilo libre de puntos o programación tácita .


Aquí hay algunos ejemplos:


 let add xy = x + y //  let add x = (+) x // point free let add1Times2 x = (x + 1) * 2 //  let add1Times2 = (+) 1 >> (*) 2 // point free let sum list = List.reduce (fun sum e -> sum+e) list //  let sum = List.reduce (+) // point free 

Este estilo tiene sus pros y sus contras.


Una de las ventajas es que el énfasis está en la composición de funciones de orden superior en lugar de preocuparse por los objetos de bajo nivel. Por ejemplo, " (+) 1 >> (*) 2 " es una suma explícita seguida de multiplicación. Y " List.reduce (+) " deja en claro que la operación de adición es importante, independientemente de la información de la lista.


Un estilo sin sentido le permite centrarse en el algoritmo básico e identificar características comunes en el código. La función " reduce " utilizada anteriormente es un buen ejemplo. Este tema se discutirá en una serie planificada sobre el procesamiento de listas.


Por otro lado, el uso excesivo de tal estilo puede hacer que el código sea oscuro. Los parámetros explícitos actúan como documentación y sus nombres (como "lista") facilitan la comprensión de lo que hace la función.


Como todo en programación, la mejor recomendación es preferir el enfoque que brinde la mayor claridad.


Combinadores


Los " combinadores " se denominan funciones cuyo resultado depende solo de sus parámetros. Esto significa que no hay dependencia del mundo exterior y, en particular, ninguna otra función o valor global puede afectarlos.


En la práctica, esto significa que las funciones combinatorias están limitadas por una combinación de sus parámetros de varias maneras.


Ya hemos visto varios combinadores: una tubería y un operador de composición. Si observa sus definiciones, está claro que todo lo que hacen es reordenar los parámetros de varias maneras.


 let (|>) xf = fx //  pipe let (<|) fx = fx //  pipe let (>>) fgx = g (fx) //   let (<<) gfx = g (fx) //   

Por otro lado, funciones como "printf", aunque primitivas, no son combinadores porque dependen del mundo exterior (E / S).


Pájaros combinatorios


Los combinadores son la base de toda una sección de lógica (naturalmente llamada "lógica combinatoria"), que fue inventada muchos años antes que las computadoras y los lenguajes de programación. La lógica combinatoria tiene una gran influencia en la programación funcional.


Para obtener más información sobre los combinadores y la lógica combinatoria, recomiendo el libro de Raymond Smullyan "To Mock a Mockingbird". En él, explica otros combinadores y les da fantasiosamente nombres de pájaros . Estos son algunos ejemplos de combinadores estándar y sus nombres de aves:


 let I x = x //  ,  Idiot bird let K xy = x // the Kestrel let M x = x >> x // the Mockingbird let T xy = yx // the Thrush ( !) let Q xyz = y (xz) // the Queer bird ( !) let S xyz = xz (yz) // The Starling //   ... let rec Y fx = f (Y f) x // Y-,  Sage bird 

Los nombres de las letras son bastante estándar, por lo que puede referirse al combinador K a cualquiera que esté familiarizado con esta terminología.


Resulta que muchos patrones de programación comunes se pueden representar a través de estos combinadores estándar. Por ejemplo, Kestrel es un patrón regular en la interfaz fluida donde haces algo pero devuelves el objeto original. Thrush es una tubería, Queer es una composición directa, y el combinador en Y hace un excelente trabajo al crear funciones recursivas.


De hecho, existe un teorema bien conocido de que cualquier función computable puede construirse utilizando solo dos combinadores básicos, Kestrel y Starling.


Bibliotecas combinatorias


Las bibliotecas combinatorias son bibliotecas que exportan muchas funciones combinatorias que están diseñadas para ser compartidas. Un usuario de dicha biblioteca puede combinar fácilmente funciones para obtener funciones aún más grandes y complejas, como los cubos fácilmente.


Una biblioteca combinada bien diseñada le permite centrarse en funciones de alto nivel y ocultar el "ruido" de bajo nivel. Ya hemos visto su poder en varios ejemplos en la serie "por qué usar F #", y el módulo List está lleno de tales funciones, " fold " y " map " también son combinadores si lo piensas.


Otra ventaja de los combinadores es que son el tipo de función más seguro. Porque no tienen dependencias del mundo exterior, no pueden cambiar cuando cambia el entorno global. Una función que lee un valor global o usa funciones de biblioteca puede romperse o cambiar entre llamadas si el contexto cambia. Esto nunca le sucederá a los combinadores.


En F #, las bibliotecas de combinador están disponibles para analizar (FParsec), crear HTML, probar marcos, etc. Discutiremos y usaremos combinadores más adelante en la próxima serie.


Funciones recursivas


A menudo, una función necesita referirse a sí misma desde su cuerpo. Un ejemplo clásico es la función de Fibonacci.


 let fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

Desafortunadamente, esta función no podrá compilar:


 error FS0039: The value or constructor 'fib' is not defined 

Debe decirle al compilador que esta es una función recursiva que utiliza la palabra clave rec .


 let rec fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

Las funciones recursivas y las estructuras de datos son muy comunes en la programación funcional, y espero dedicar una serie completa a este tema más adelante.


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


All Articles