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:
- Uma função com o nome "
printTwoParameters
" é printTwoParameters
, mas aceita apenas um parâmetro: "x". - 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.
- Finalmente, a função local recém-criada é retornada.
- 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.