Pensamiento funcional Parte 4

Después de una breve digresión sobre los tipos básicos, podemos volver a las funciones nuevamente. En particular, al enigma mencionado anteriormente: si una función matemática puede tomar solo un parámetro, ¿cómo puede una función en F # tomar más parámetros? Más detalles debajo del corte!




La respuesta es bastante simple: una función con varios parámetros se reescribe como una serie de nuevas funciones, cada una de las cuales solo toma un parámetro. El compilador realiza esta operación automáticamente, y se llama " curry ", en honor a Haskell Curry, un matemático que influyó significativamente en el desarrollo de la programación funcional.


Para ver cómo funciona el curry en la práctica, usemos un ejemplo de código simple que imprime dos números:


//   let printTwoParameters xy = printfn "x=%iy=%i" xy 

De hecho, el compilador lo reescribe en aproximadamente la siguiente forma:


 //    let printTwoParameters x = //    let subFunction y = printfn "x=%iy=%i" xy //  ,    subFunction //   

Considere este proceso con más detalle:


  1. Se printTwoParameters una función con el nombre " printTwoParameters ", pero se acepta un solo parámetro: "x".
  2. Se crea una función local dentro de ella, que también toma solo un parámetro: "y". Tenga en cuenta que la función local usa el parámetro "x", pero x no se le pasa como argumento. "x" tiene un alcance tal que una función anidada puede verlo y usarlo sin la necesidad de pasarlo.
  3. Finalmente, se devuelve la función local recién creada.
  4. La función devuelta se aplica al argumento "y". El parámetro "x" está cerrado, de modo que la función devuelta solo necesita el parámetro "y" para completar su lógica.

Al reescribir las funciones de esta manera, el compilador se asegura de que cada función acepte solo un parámetro, según sea necesario. Por lo tanto, usando " printTwoParameters ", podría pensar que esta es una función con dos parámetros, pero en realidad se usa una función con un solo parámetro. Puede verificar esto pasándole solo un argumento en lugar de dos:


 //     printTwoParameters 1 //    val it : (int -> unit) = <fun:printTwoParameters@286-3> 

Si lo calculamos con un argumento, no obtendremos un error: se devolverá la función.


Entonces, esto es lo que realmente sucede cuando se llama a printTwoParameters con dos argumentos:


  • printTwoParameters se printTwoParameters con el primer argumento (x)
  • printTwoParameters devuelve una nueva función en la que se cierra "x".
  • Luego se llama a una nueva función con el segundo argumento (y)

Aquí hay un ejemplo de una versión normal y paso a paso:


 //   let x = 6 let y = 99 let intermediateFn = printTwoParameters x //  -  // x   let result = intermediateFn y //     let result = (printTwoParameters x) y //   let result = printTwoParameters xy 

Aquí hay otro ejemplo:


 //  let addTwoParameters xy = x + y //   let addTwoParameters x = //   ! let subFunction y = x + y //      subFunction //   //       let x = 6 let y = 99 let intermediateFn = addTwoParameters x //  -  // x   let result = intermediateFn y //   let result = addTwoParameters xy 

Una vez más, una "función con dos parámetros" es en realidad una función con un parámetro, que devuelve una función intermedia.


Pero espera, ¿qué pasa con el operador + ? ¿Es esta una operación binaria que debe tomar dos parámetros? No, también es curry, como otras funciones. Esta es una función llamada " + " que toma un parámetro y devuelve una nueva función intermedia, al igual que addTwoParameters arriba.


Cuando escribimos la expresión x+y , el compilador reordena el código de tal manera que convierte el infijo a (+) xy , que es una función llamada + que toma dos parámetros. Tenga en cuenta que la función "+" necesita paréntesis para indicar que se utiliza como una función regular y no como un operador infijo.


Finalmente, una función con dos parámetros, llamada + , se trata como cualquier otra función con dos parámetros.


 //         let x = 6 let y = 99 let intermediateFn = (+) x //   ""  ""   let result = intermediateFn y //        let result = (+) xy //       let result = x + y 

Y sí, esto funciona para todos los demás operadores y funciones integradas como printf .


 //    let result = 3 * 5 //    - let intermediateFn = (*) 3 //  ""  3   let result = intermediateFn 5 //    printfn let result = printfn "x=%iy=%i" 3 5 // printfn   - let intermediateFn = printfn "x=%iy=%i" 3 // "3"   let result = intermediateFn 5 

Firmas de funciones al curry


Ahora que sabemos cómo funcionan las funciones curry, es interesante saber cómo serán sus firmas.


Volviendo al primer ejemplo, " printTwoParameter ", vimos que la función tomó un argumento y devolvió una función intermedia. La función intermedia también tomó un argumento y no devolvió nada (es decir, unit ). Por lo tanto, la función intermedia era de tipo int->unit . En otras palabras, el dominio printTwoParameters es int , y el rango es int->unit . Poniendo todo junto veremos la firma final:


 val printTwoParameters : int -> (int -> unit) 

Si calcula una implementación explícitamente currificada, puede ver los corchetes en la firma, pero si calcula una implementación ordinaria, implícitamente currificada, no habrá corchetes:


 val printTwoParameters : int -> int -> unit 

Los soportes son opcionales. Pero pueden representarse en la mente para simplificar la percepción de las firmas de funciones.


¿Y cuál es la diferencia entre una función que devuelve una función intermedia y una función regular con dos parámetros?


Aquí hay una función con un parámetro que devuelve otra función:


 let add1Param x = (+) x // signature is = int -> (int -> int) 

Y aquí hay una función con dos parámetros que devuelve un valor simple:


 let add2Params xy = (+) xy // signature is = int -> int -> int 

Sus firmas son ligeramente diferentes, pero en un sentido práctico no hay mucha diferencia entre ellas, excepto por el hecho de que la segunda función se curry automáticamente.


Funciones con más de dos parámetros.


¿Cómo funciona el curry para funciones con más de dos parámetros? Del mismo modo: para cada parámetro, excepto el último, la función devuelve una función intermedia que cierra el parámetro anterior.


Considere este difícil ejemplo. He declarado explícitamente los tipos de parámetros, pero la función no hace nada.


 let multiParamFn (p1:int)(p2:bool)(p3:string)(p4:float)= () //   let intermediateFn1 = multiParamFn 42 // multoParamFn  int   (bool -> string -> float -> unit) // intermediateFn1  bool //   (string -> float -> unit) let intermediateFn2 = intermediateFn1 false // intermediateFn2  string //   (float -> unit) let intermediateFn3 = intermediateFn2 "hello" // intermediateFn3 float //     (unit) let finalResult = intermediateFn3 3.141 

Firma de toda la función:


 val multiParamFn : int -> bool -> string -> float -> unit 

y firmas de funciones intermedias:


 val intermediateFn1 : (bool -> string -> float -> unit) val intermediateFn2 : (string -> float -> unit) val intermediateFn3 : (float -> unit) val finalResult : unit = () 

La firma de la función puede decirle cuántos parámetros toma la función: solo cuente el número de flechas fuera de los corchetes. Si la función acepta o devuelve otra función, habrá más flechas, pero estarán entre paréntesis y pueden ignorarse. Aquí hay algunos ejemplos:


 int->int->int // 2  int  int string->bool->int //   string,  - bool, //  int int->string->bool->unit //   (int,string,bool) //    (unit) (int->string)->int //   ,  // ( int  string) //   int (int->string)->(int->bool) //   (int  string) //   (int  bool) 

Múltiples dificultades de parámetros


Hasta que comprenda la lógica detrás del curry, producirá algunos resultados inesperados. Recuerde que no obtendrá un error si ejecuta la función con menos argumentos de los esperados. En cambio, obtienes una función parcialmente aplicada. Si luego usa la función parcialmente aplicada en el contexto donde se espera el valor, puede obtener un oscuro error del compilador.


Considere una función que es inofensiva a primera vista:


 //   let printHello() = printfn "hello" 

¿Qué crees que sucederá si lo invocas como se muestra a continuación? ¿Se imprimirá "hola" en la consola? Intenta adivinar antes de la ejecución. Sugerencia: mira la firma de la función.


 //   printHello 

Contrariamente a lo esperado, no habrá llamada. La función original espera unit como un argumento que no se ha pasado. Por lo tanto, se obtuvo una función parcialmente aplicada (en este caso, sin argumentos).


¿Qué hay de este caso? ¿Se compilará?


 let addXY xy = printfn "x=%iy=%i" x x + y 

Si lo ejecuta, el compilador se quejará de la línea con printfn .


 printfn "x=%iy=%i" x //^^^^^^^^^^^^^^^^^^^^^ //warning FS0193: This expression is a function value, ie is missing //arguments. Its type is ^a -> unit. 

Si no se entiende el curry, este mensaje puede ser muy críptico. El hecho es que todas las expresiones que se evalúan por separado (es decir, no se utilizan como valor de retorno o están vinculadas a algo por medio de "let") deben evaluarse en el valor unit . En este caso, no se calcula en el valor unit , sino que devuelve una función. Esta es una larga forma de decir que printfn tiene un argumento.


En la mayoría de los casos, ocurren errores como este al interactuar con una biblioteca del mundo .NET. Por ejemplo, el método Readline de la clase TextReader debe tomar un parámetro de unit . A menudo puede olvidarse de esto y no poner corchetes, en este caso no puede obtener un error del compilador en el momento de la "llamada", pero aparecerá cuando intente interpretar el resultado como una cadena.


 let reader = new System.IO.StringReader("hello"); let line1 = reader.ReadLine // ,    printfn "The line is %s" line1 //    // ==> error FS0001: This expression was expected to have // type string but here has type unit -> string let line2 = reader.ReadLine() // printfn "The line is %s" line2 //   

En el código anterior, line1 es solo un puntero o delegado al método Readline , no una cadena, como es de esperar. El uso de () en reader.ReadLine() realidad llamará a la función.


Demasiadas opciones


Puede obtener mensajes igualmente crípticos si pasa demasiados parámetros a una función. Algunos ejemplos de pasar demasiados parámetros a printf :


 printfn "hello" 42 // ==> error FS0001: This expression was expected to have // type 'a -> 'b but here has type unit printfn "hello %i" 42 43 // ==> Error FS0001: Type mismatch. Expecting a 'a -> 'b -> 'c // but given a 'a -> unit printfn "hello %i %i" 42 43 44 // ==> Error FS0001: Type mismatch. Expecting a 'a->'b->'c->'d // but given a 'a -> 'b -> unit 

Por ejemplo, en el último caso, el compilador informa que se espera una cadena de formato con tres parámetros (firma 'a -> 'b -> 'c -> 'd tiene tres parámetros), pero en su lugar, se recibe una cadena con dos (para la firma 'a -> 'b -> unit dos parámetros).


En aquellos casos en los que no se usa printf , pasar una gran cantidad de parámetros a menudo significa que en una determinada etapa del cálculo, se obtuvo un valor simple, para el cual se está probando el parámetro. Al compilador le molestará que un valor simple no sea una función.


 let add1 x = x + 1 let x = add1 2 3 // ==> error FS0003: This value is not a function // and cannot be applied 

Si desglosamos la llamada general en una serie de funciones intermedias explícitas, como lo hicimos anteriormente, podemos ver qué está sucediendo exactamente.


 let add1 x = x + 1 let intermediateFn = add1 2 //   let x = intermediateFn 3 //intermediateFn  ! // ==> error FS0003: This value is not a function // and cannot be applied 

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


All Articles