Continuamos nossa série de artigos sobre programação funcional em F #. Hoje temos um tópico muito interessante: a definição de funções. Inclusive, vamos falar sobre funções anônimas, funções sem parâmetros, funções recursivas, combinadores e muito mais. Olhe embaixo do gato!

Definição de Função
Já sabemos como criar funções regulares usando a sintaxe "let":
let add xy = x + y
Neste artigo, veremos outras maneiras de criar funções, além de dicas para defini-las.
Funções anônimas (lambdas)
Se você estiver familiarizado com lambdas em outros idiomas, os parágrafos a seguir parecerão familiares. Funções anônimas (ou "expressões lambda") são definidas da seguinte maneira:
fun parameter1 parameter2 etc -> expression
Comparado às lambdas do C #, há duas diferenças:
- lambdas deve começar com a palavra-chave
fun
, que não é necessária em C # - seta única
->
usada ->
, em vez de dupla =>
de C #.
Definição lambda da função de adição:
let add = fun xy -> x + y
Mesma função na forma tradicional:
let add xy = x + y
Lambdas são freqüentemente usadas na forma de pequenas expressões ou quando não há desejo de definir uma função separada para uma expressão. Como você já viu, ao trabalhar com listas, isso não é incomum.
// let add1 i = i + 1 [1..10] |> List.map add1 // [1..10] |> List.map (fun i -> i + 1)
Observe que os parênteses devem ser usados em torno das lambdas.
Lambdas também são usados quando uma função claramente diferente é necessária. Por exemplo, o " adderGenerator
" discutido anteriormente, discutido anteriormente, pode ser reescrito usando lambdas.
// let adderGenerator x = (+) x // let adderGenerator x = fun y -> x + y
A versão lambda é um pouco mais longa, mas imediatamente deixa claro que uma função intermediária será retornada.
Lambdas podem ser aninhadas. Outro exemplo de uma definição adderGenerator
, desta vez apenas em lambdas.
let adderGenerator = fun x -> (fun y -> x + y)
Você está claro que todas as três definições são equivalentes?
let adderGenerator1 xy = x + y let adderGenerator2 x = fun y -> x + y let adderGenerator3 = fun x -> (fun y -> x + y)
Caso contrário, leia novamente o capítulo sobre curry . Isso é muito importante para a compreensão!
Correspondência de padrões
Quando uma função é definida, é possível passar parâmetros explicitamente, como nos exemplos acima, mas também é possível comparar com um modelo diretamente na seção de parâmetros. Em outras palavras, a seção de parâmetros pode conter padrões (padrões correspondentes), e não apenas identificadores!
O exemplo a seguir demonstra o uso de padrões em uma definição de função:
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
Esse tipo de comparação pode ocorrer apenas quando a correspondência é sempre decidível. Por exemplo, você não pode combinar tipos e listas de união dessa maneira, porque alguns casos não podem ser correspondidos.
let f3 (x::xs) = // printfn "first element is=%A" x
O compilador emitirá um aviso sobre correspondência incompleta (uma lista vazia causará um erro no tempo de execução na entrada desta função).
Erro comum: tuplas vs. muitos parâmetros
Se você vem de uma linguagem do tipo C, a tupla usada como único argumento da função pode dolorosamente se assemelhar a uma função de vários parâmetros. Mas isso não é a mesma coisa! Como observei anteriormente, se você vir uma vírgula, provavelmente é uma tupla. Os parâmetros são separados por espaços.
Exemplo de confusão:
// let addTwoParams xy = x + y // - let addTuple aTuple = let (x,y) = aTuple x + y // // let addConfusingTuple (x,y) = x + y
- A primeira definição, "
addTwoParams
", usa dois parâmetros, separados por um espaço. - A segunda definição, "
addTuple
", usa um parâmetro. Este parâmetro liga "x" e "y" da tupla e os soma. - A terceira definição, "
addConfusingTuple
", usa um parâmetro como " addTuple
", mas o truque é que essa tupla seja descompactada (correspondida ao padrão) e vinculada como parte da definição de parâmetro usando a correspondência de padrões. Nos bastidores, tudo acontece exatamente da mesma forma que no addTuple
.
Vejamos as assinaturas (sempre olhe para elas, se você não tiver certeza de alguma coisa).
val addTwoParams : int -> int -> int // val addTuple : int * int -> int // tuple->int val addConfusingTuple : int * int -> int // tuple->int
E agora aqui:
// addTwoParams 1 2 // ok -- addTwoParams (1,2) // error - // => error FS0001: This expression was expected to have type // int but here has type 'a * 'b
Aqui vemos um erro na segunda chamada.
Primeiramente, o compilador trata (1,2)
como uma tupla generalizada do formulário ('a * 'b)
, que tenta passar como o primeiro parâmetro para addTwoParams
. Depois disso, ele reclama que o primeiro parâmetro esperado addTwoParams
não addTwoParams
int
, mas foi feita uma tentativa de passar uma tupla.
Para fazer uma tupla, use uma vírgula!
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
E vice-versa, se você passar vários argumentos para uma função aguardando uma tupla, também receberá um erro incompreensível.
addConfusingTuple 1 2 // error -- // => error FS0003: This value is not a function and // cannot be applied
Dessa vez, o compilador decidiu que, depois que dois argumentos fossem addConfusingTuple
, o addConfusingTuple
deveria ser curry. E a entrada " addConfusingTuple 1
" é um aplicativo parcial e deve retornar uma função intermediária. Tentar chamar essa função intermediária com o parâmetro "2" gerará um erro, porque não há função intermediária! Vemos o mesmo erro do capítulo sobre curry, onde discutimos problemas com muitos parâmetros.
Por que não usar tuplas como parâmetros?
A discussão das tuplas acima mostra outra maneira de definir funções com muitos parâmetros: em vez de passá-las separadamente, todos os parâmetros podem ser montados em uma estrutura. No exemplo abaixo, a função usa um único parâmetro - uma tupla de três elementos.
let f (x,y,z) = x + y * z // - int * int * int -> int // f (1,2,3)
Note-se que a assinatura é diferente da assinatura de uma função com três parâmetros. Há apenas uma seta, um parâmetro e asteriscos apontando para a tupla (int*int*int)
.
Quando é necessário enviar argumentos com parâmetros separados e quando uma tupla?
- Quando as tuplas são significativas em si mesmas. Por exemplo, para operações no espaço tridimensional, as tuplas triplas serão mais convenientes do que três coordenadas separadamente.
- Às vezes, as tuplas são usadas para combinar dados que devem ser armazenados juntos em uma única estrutura. Por exemplo, os métodos
TryParse
da biblioteca .NET retornam o resultado e uma variável booleana como uma tupla. Mas para armazenar uma grande quantidade de dados relacionados, é melhor definir uma classe ou registro ( registro .
Caso especial: Tuplas e funções da biblioteca .NET
Ao chamar bibliotecas .NET, vírgulas são muito comuns!
Todos eles aceitam tuplas e as chamadas são iguais às do C #:
// System.String.Compare("a","b") // System.String.Compare "a" "b"
O motivo é que as funções do .NET clássico não são curry e não podem ser parcialmente aplicadas. Todos os parâmetros sempre devem ser transmitidos imediatamente, e a maneira mais óbvia é usar uma tupla.
Observe que essas chamadas parecem apenas transferir tuplas, mas esse é realmente um caso especial. Você não pode passar tuplas reais para essas funções:
let tuple = ("a","b") System.String.Compare tuple // error System.String.Compare "a","b" // error
Se você deseja aplicar parcialmente as funções .NET, basta escrever sobre elas, como foi feito anteriormente ou como mostrado abaixo:
// let strCompare xy = System.String.Compare(x,y) // let strCompareWithB = strCompare "B" // ["A";"B";"C"] |> List.map strCompareWithB
Guia para selecionar parâmetros individuais e agrupados
A discussão das tuplas leva a um tópico mais geral: quando os parâmetros devem ser separados e quando agrupados?
Você deve prestar atenção em como o F # difere do C # nesse sentido. Em C #, todos os parâmetros são sempre passados, portanto, essa pergunta nem sequer aparece lá! No F #, devido à aplicação parcial, apenas alguns dos parâmetros podem ser representados; portanto, é necessário distinguir entre o caso em que os parâmetros devem ser combinados e o caso em que são independentes.
Recomendações gerais sobre como estruturar parâmetros ao projetar suas próprias funções.
- No caso geral, é sempre melhor usar parâmetros separados em vez de passar uma estrutura, seja uma tupla ou um registro. Isso permite um comportamento mais flexível, como aplicação parcial.
- Porém, quando um grupo de parâmetros precisa ser passado por vez, algum tipo de mecanismo de agrupamento deve ser usado.
Em outras palavras, ao desenvolver uma função, pergunte a si mesmo: "Posso fornecer esse parâmetro separadamente?" Se a resposta for não, os parâmetros devem ser agrupados.
Vejamos alguns exemplos:
// . // , 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 = //
Por fim, verifique se a ordem dos parâmetros ajudará na aplicação parcial (consulte o manual aqui ). Por exemplo, por que coloquei myCredentials
antes de aName
na última função?
Funções sem parâmetros
Às vezes, você pode precisar de uma função que não aceita nenhum parâmetro. Por exemplo, você precisa da função "olá mundo", que pode ser chamada várias vezes. Como mostrado na seção anterior, a definição ingênua não funciona.
let sayHello = printfn "Hello World!" //
Mas isso pode ser corrigido adicionando um parâmetro de unidade à função ou usando um lambda.
let sayHello() = printfn "Hello World!" // let sayHello = fun () -> printfn "Hello World!" //
Depois disso, a função deve sempre ser chamada com o argumento unit
:
// sayHello()
O que acontece com frequência ao interagir com bibliotecas .NET:
Console.ReadLine() System.Environment.GetCommandLineArgs() System.IO.Directory.GetCurrentDirectory()
Lembre-se, chame-os com parâmetros de unit
!
Definindo novos operadores
Você pode definir funções usando um ou mais caracteres do operador (consulte a documentação para obter uma lista de caracteres):
// let (.*%) xy = x + y + 1
Você deve usar parênteses nos caracteres para definir a função.
Os operadores que começam com *
requerem um espaço entre parênteses e *
, porque em F # (*
atua como o início de um comentário (como /*...*/
em C #):
let ( *+* ) xy = x + y + 1
Uma vez definida, uma nova função pode ser usada da maneira usual se estiver entre colchetes:
let result = (.*%) 2 3
Se a função for usada com dois parâmetros, você poderá usar o registro do operador infix sem parênteses.
let result = 2 .*% 3
Você também pode definir operadores de prefixo começando com !
ou ~
(com algumas restrições, consulte a documentação )
let (~%%) (s:string) = s.ToCharArray() // let result = %% "hello"
No F #, definir instruções é uma operação bastante comum e muitas bibliotecas exportam instruções com nomes como >=>
e <*>
.
Estilo sem ponto
Já vimos muitos exemplos de funções que careciam dos parâmetros mais recentes para reduzir o nível de caos. Esse estilo é chamado de estilo sem ponto ou programação tácita .
Aqui estão alguns exemplos:
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
Esse estilo tem seus prós e contras.
Uma das vantagens é que a ênfase está na composição de funções de ordem superior, em vez de mexer com objetos de baixo nível. Por exemplo, " (+) 1 >> (*) 2
" é uma adição explícita seguida por multiplicação. E " List.reduce (+)
" deixa claro que a operação de adição é importante, independentemente das informações da lista.
Um estilo inútil permite que você se concentre no algoritmo básico e identifique recursos comuns no código. A função " reduce
" usada acima é um bom exemplo. Este tópico será discutido em uma série planejada sobre o processamento de listas.
Por outro lado, o uso excessivo desse estilo pode tornar o código obscuro. Parâmetros explícitos agem como documentação e seus nomes (como "lista") facilitam a compreensão do que a função faz.
Como tudo na programação, a melhor recomendação é preferir a abordagem que forneça mais clareza.
Combinadores
" Combinadores " são chamados de funções cujo resultado depende apenas de seus parâmetros. Isso significa que não há dependência do mundo exterior e, em particular, nenhuma outra função ou valores globais podem afetá-los.
Na prática, isso significa que as funções combinatórias são limitadas por uma combinação de seus parâmetros de várias maneiras.
Já vimos vários combinadores: um operador de tubo e composição. Se você observar as definições deles, ficará claro que tudo o que eles fazem é reordenar os parâmetros de várias maneiras.
let (|>) xf = fx // pipe let (<|) fx = fx // pipe let (>>) fgx = g (fx) // let (<<) gfx = g (fx) //
Por outro lado, funções como "printf", embora primitivas, não são combinadoras porque são dependentes do mundo externo (E / S).
Aves combinatórias
Os combinadores são a base de toda uma seção da lógica (naturalmente chamada de "lógica combinatória"), que foi inventada muitos anos antes dos computadores e das linguagens de programação. A lógica combinatória tem uma influência muito grande na programação funcional.
Para aprender mais sobre combinadores e lógica combinatória, recomendo o livro de Raymond Smullyan, "To Mock a Mockingbird". Nele, ele explica outros combinadores e fantasia-lhes nomes de pássaros . Aqui estão alguns exemplos de combinadores padrão e seus nomes de pássaros:
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
Os nomes das letras são bastante padrão, portanto, você pode consultar o combinador K para qualquer pessoa familiarizada com esta terminologia.
Acontece que muitos padrões de programação comuns podem ser representados por esses combinadores padrão. Por exemplo, o Kestrel é um padrão regular na interface fluente em que você faz algo, mas retorna o objeto original. Thrush é um pipe, Queer é uma composição direta e o combinador Y faz um excelente trabalho na criação de funções recursivas.
De fato, existe um teorema bem conhecido de que qualquer função computável pode ser construída usando apenas dois combinadores básicos, Kestrel e Starling.
Bibliotecas Combinatórias
Bibliotecas combinatórias são bibliotecas que exportam muitas funções combinatórias projetadas para serem compartilhadas. Um usuário dessa biblioteca pode facilmente combinar funções para obter funções ainda maiores e mais complexas, como cubos facilmente.
Uma biblioteca combinadora bem projetada permite que você se concentre em funções de alto nível e oculte "ruído" de baixo nível. Já vimos seu poder em vários exemplos da série "por que usar F #", e o módulo List
está cheio dessas funções, " fold
" e " map
" também são combinadores, se você pensar bem.
Outra vantagem dos combinadores é que eles são o tipo mais seguro de função. Porque eles não têm dependências do mundo exterior e não podem mudar quando o ambiente global muda. Uma função que lê um valor global ou usa funções de biblioteca pode interromper ou mudar entre chamadas, se o contexto mudar. Isso nunca acontecerá aos combinadores.
No F #, as bibliotecas combinadoras estão disponíveis para análise (FParsec), criação de HTML, estruturas de teste etc. Discutiremos e usaremos combinadores mais adiante na próxima série.
Funções recursivas
Muitas vezes, uma função precisa se referir a si mesma a partir de seu corpo. Um exemplo clássico é a função Fibonacci.
let fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2)
Infelizmente, esta função não poderá compilar:
error FS0039: The value or constructor 'fib' is not defined
Você deve informar ao compilador que esta é uma função recursiva usando a palavra-chave rec
.
let rec fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2)
Funções recursivas e estruturas de dados são muito comuns na programação funcional, e espero dedicar uma série inteira a esse tópico posteriormente.
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.