Pensamento funcional. Parte 4

Após uma pequena digressão nos tipos básicos, podemos retornar às funções novamente. Em particular, para o enigma mencionado anteriormente: se uma função matemática pode usar apenas um parâmetro, como uma função em F # pode usar mais parâmetros? Mais detalhes sob o corte!




A resposta é bastante simples: uma função com vários parâmetros é reescrita como uma série de novas funções, cada uma das quais usa apenas um parâmetro. O compilador executa essa operação automaticamente e é chamado de " currying ", em homenagem a Haskell Curry, um matemático que influenciou significativamente o desenvolvimento da programação funcional.


Para ver como o curry funciona na prática, vamos usar um exemplo de código simples que imprime dois números:


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

De fato, o compilador o reescreve aproximadamente da seguinte forma:


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

Considere esse processo com mais detalhes:


  1. Uma função com o nome " printTwoParameters " é printTwoParameters , mas aceita apenas um parâmetro: "x".
  2. Uma função local é criada dentro dela, que também aceita apenas um parâmetro: "y". Observe que a função local usa o parâmetro "x", mas x não é passado para ele como argumento. "x" está em um escopo que uma função aninhada pode vê-lo e usá-lo sem a necessidade de passá-lo.
  3. Finalmente, a função local recém-criada é retornada.
  4. A função retornada é então aplicada ao argumento "y". O parâmetro "x" é fechado para que a função retornada precise apenas do parâmetro "y" para concluir sua lógica.

Reescrevendo funções dessa maneira, o compilador garante que cada função aceite apenas um parâmetro, conforme necessário. Portanto, usando " printTwoParameters ", você pode pensar que esta é uma função com dois parâmetros, mas, de fato, uma função com apenas um parâmetro é usada. Você pode verificar isso passando apenas um argumento em vez de dois:


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

Se o calcularmos com um argumento, não obteremos um erro - a função será retornada.


Então, aqui está o que realmente acontece quando printTwoParameters é chamado com dois argumentos:


  • printTwoParameters é printTwoParameters com o primeiro argumento (x)
  • printTwoParameters retorna uma nova função na qual "x" está fechado.
  • Em seguida, uma nova função é chamada com o segundo argumento (y)

Aqui está um exemplo de versões normais e passo a passo:


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

Aqui está outro exemplo:


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

Novamente, uma "função com dois parâmetros" é na verdade uma função com um parâmetro, que retorna uma função intermediária.


Mas espere, e o operador + ? Esta é uma operação binária que deve receber dois parâmetros? Não, também é curry, como outras funções. Essa é uma função chamada " + " que pega um parâmetro e retorna uma nova função intermediária, como addTwoParameters acima.


Quando escrevemos a expressão x+y , o compilador reordena o código de forma a converter o infixo em (+) xy , que é uma função chamada + que usa dois parâmetros. Observe que a função “+” precisa de parênteses para indicar que é usada como uma função regular, e não como um operador de infix.


Finalmente, uma função com dois parâmetros, chamada + , é tratada como qualquer outra função com dois parâmetros.


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

E sim, isso funciona para todos os outros operadores e funções printf 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 

Assinaturas de função ao curry


Agora que sabemos como funcionam as funções ao curry, é interessante saber como serão as assinaturas.


Voltando ao primeiro exemplo, " printTwoParameter ", vimos que a função pegou um argumento e retornou uma função intermediária. A função intermediária também pegou um argumento e não retornou nada (ou seja, unit ). Portanto, a função intermediária era do tipo int->unit . Em outras palavras, o domínio printTwoParameters é int e range é int->unit . Juntando tudo, veremos a assinatura final:


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

Se você calcular a implementação explicada, poderá ver os colchetes na assinatura, mas se calcular a implementação ordinária implicada comum, não haverá colchetes:


 val printTwoParameters : int -> int -> unit 

Os suportes são opcionais. Mas eles podem ser representados na mente para simplificar a percepção das assinaturas de funções.


E qual é a diferença entre uma função que retorna uma função intermediária e uma função regular com dois parâmetros?


Aqui está uma função com um parâmetro que retorna outra função:


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

E aqui está uma função com dois parâmetros que retorna um valor simples:


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

Suas assinaturas são ligeiramente diferentes, mas, no sentido prático, não há muita diferença entre elas, exceto pelo fato de que a segunda função é automaticamente ativa.


Funções com mais de dois parâmetros


Como o curry funciona para funções com mais de dois parâmetros? Da mesma maneira: para cada parâmetro, exceto o último, a função retorna uma função intermediária que fecha o parâmetro anterior.


Considere este exemplo difícil. Declarei explicitamente os tipos de parâmetros, mas a função não faz 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 

Assinatura de toda a função:


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

e assinaturas de funções intermediárias:


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

A assinatura da função pode informar quantos parâmetros a função leva: basta contar o número de setas fora dos colchetes. Se a função aceitar ou retornar outra função, haverá mais setas, mas elas estarão entre colchetes e poderão ser ignoradas. Aqui estão alguns exemplos:


 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) 

Dificuldades com vários parâmetros


Até você entender a lógica por trás do curry, ele produzirá alguns resultados inesperados. Lembre-se de que você não receberá um erro se executar a função com menos argumentos do que o esperado. Em vez disso, você obtém uma função parcialmente aplicada. Se você usar a função parcialmente aplicada no contexto em que o valor é esperado, poderá obter um erro obscuro do compilador.


Considere uma função que é inofensiva à primeira vista:


 //   let printHello() = printfn "hello" 

O que você acha que acontecerá se você o chamar, como mostrado abaixo? O "olá" será impresso no console? Tente adivinhar antes da execução. Dica: veja a assinatura da função.


 //   printHello 

Ao contrário das expectativas, não haverá chamada. A função original espera unit como um argumento que não foi passado. Portanto, uma função parcialmente aplicada foi obtida (neste caso, sem argumentos).


E esse caso? Será compilado?


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

Se você executá-lo, o compilador irá reclamar da linha com printfn .


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

Se não houver entendimento do curry, essa mensagem pode ser muito enigmática. O fato é que todas as expressões avaliadas separadamente (ou seja, não são usadas como um valor de retorno ou estão vinculadas a algo por meio de "let") devem ser avaliadas no valor unit . Nesse caso, não é calculado no valor unit , mas retorna uma função. É um longo caminho para dizer que printfn está faltando um argumento.


Na maioria dos casos, erros como esse acontecem ao interagir com uma biblioteca do mundo .NET. Por exemplo, o método Readline da classe TextReader deve Readline um parâmetro de unit . Muitas vezes você pode esquecer isso e não colocar colchetes; nesse caso, não é possível obter um erro do compilador no momento da "chamada", mas ele aparecerá quando você tenta interpretar o resultado como uma sequência.


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

No código acima, a line1 é apenas um ponteiro ou delegado para o método Readline , não uma string, como você poderia esperar. Usar () no reader.ReadLine() realmente chamará a função.


Muitas opções


Você pode receber mensagens igualmente enigmáticas se passar muitos parâmetros para uma função. Alguns exemplos de passagem de muitos parâmetros para 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 exemplo, no último caso, o compilador relata que uma sequência de formato com três parâmetros é esperada (a assinatura 'a -> 'b -> 'c -> 'd possui três parâmetros), mas, em vez disso, uma sequência com dois é recebida (para a assinatura 'a -> 'b -> unit dois parâmetros).


Nos casos em que printf não é usado, passar um grande número de parâmetros geralmente significa que, em um certo estágio do cálculo, foi obtido um valor simples, para o qual o parâmetro está sendo tentado. O compilador ressentirá que um valor simples não é uma função.


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

Se dividirmos a chamada geral em uma série de funções intermediárias explícitas, como fizemos anteriormente, podemos ver o que exatamente está errado.


 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 Adicionais


Existem muitos tutoriais para F #, incluindo materiais para quem vem com experiência em C # ou Java. Os links a seguir podem ser úteis à medida que você avança no F #:



Várias outras maneiras de começar a aprender F # também são descritas.


Finalmente, a comunidade F # é muito amigável para iniciantes. Há um bate-papo muito ativo no Slack, suportado pela F # Software Foundation, com salas para iniciantes nas quais você pode participar livremente . É altamente recomendável que você faça isso!


Não se esqueça de visitar o site da comunidade de língua russa F # ! Se você tiver alguma dúvida sobre o aprendizado de um idioma, teremos prazer em discuti-los nas salas de bate-papo:



Sobre autores de tradução


Traduzido por @kleidemos
As mudanças de tradução e editoriais foram feitas pelos esforços da comunidade de desenvolvedores de F # de língua russa . Agradecemos também a @schvepsss e @shwars pela preparação deste artigo para publicação.

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


All Articles