Compartilhar via


Introdução aos conceitos de programação funcional em F#

A programação funcional é um estilo de programação que enfatiza o uso de funções e dados imutáveis. A programação funcional tipada é quando a programação funcional é combinada com tipos estáticos, como com F#. Em geral, os seguintes conceitos são enfatizados na programação funcional:

  • Funções são os conceitos primários que você usa
  • Expressões ao invés de instruções
  • Valores imutáveis em variáveis
  • Programação declarativa sobre programação imperativa

Ao longo desta série, você explorará conceitos e padrões na programação funcional usando F#. Ao longo do caminho, você aprenderá um pouco de F# também.

Terminologia

A programação funcional, como outros paradigmas de programação, vem com um vocabulário que você eventualmente precisará aprender. Aqui estão alguns termos comuns que você verá o tempo todo:

  • Função – Uma função é uma construção que produzirá uma saída quando uma entrada for fornecida. Mais formalmente, ele mapeia um item de um conjunto para outro. Esse formalismo é levantado no concreto de várias maneiras, especialmente ao usar funções que operam em coleções de dados. É o conceito mais básico (e importante) na programação funcional.
  • Expressão – Uma expressão é um constructo no código que produz um valor. No F#, este valor deve ser vinculado ou ignorado explicitamente. Uma expressão pode ser substituída trivialmente por uma chamada de função.
  • Puridade – A puridade é uma propriedade de uma função de modo que seu valor retornado seja sempre o mesmo para os mesmos argumentos e que sua avaliação não tenha efeitos colaterais. Uma função pura depende inteiramente de seus argumentos.
  • Transparência Referencial – Transparência Referencial é uma propriedade de expressões para que possam ser substituídas por sua saída sem afetar o comportamento de um programa.
  • Imutabilidade – Imutabilidade significa que um valor não pode ser alterado no local. Isso contrasta com variáveis, que podem ser alteradas no local.

Exemplos

Os exemplos a seguir demonstram esses conceitos principais.

Funções

O constructo mais comum e fundamental na programação funcional é a função. Aqui está uma função simples que adiciona 1 a um inteiro:

let addOne x = x + 1

Sua assinatura de tipo é a seguinte:

val addOne: x:int -> int

A assinatura pode ser lida como, "addOne aceita um int nomeado x e produzirá um int". Mais formalmente, addOne é mapear um valor do conjunto de inteiros para o conjunto de inteiros. O -> token significa esse mapeamento. Em F#, você geralmente pode examinar a assinatura da função para ter uma noção do que ela faz.

Então, por que a assinatura é importante? Na programação funcional tipada, a implementação de uma função costuma ser menos importante do que a própria assinatura de tipo! O fato de adicionar addOne o valor 1 a um inteiro é interessante em tempo de execução, mas quando você está construindo um programa, o fato de ele aceitar e retornar um int é o que informa como você realmente usará essa função. Além disso, depois de usar essa função corretamente (em relação à sua assinatura de tipo), o diagnóstico de quaisquer problemas só poderá ser feito dentro do corpo da addOne função. Esse é o impulso por trás da programação funcional tipada.

Expressões

Expressões são construções que são avaliadas como um valor. Ao contrário das instruções, que executam uma ação, as expressões podem ser pensadas para executar uma ação que devolve um valor. As expressões são quase sempre usadas na programação funcional em vez de instruções.

Considere a função anterior. addOne O corpo de addOne é uma expressão:

// 'x + 1' is an expression!
let addOne x = x + 1

É o resultado dessa expressão que define o tipo de resultado da addOne função. Por exemplo, a expressão que compõe essa função pode ser alterada para ser um tipo diferente, como:string

let addOne x = x.ToString() + "1"

A assinatura da função agora é:

val addOne: x:'a -> string

Como qualquer tipo em F# pode ter ToString() chamado nele, o tipo de x foi tornado genérico (chamado generalização automática) e o tipo resultante é um string.

Expressões não são apenas os corpos das funções. Você pode ter expressões que produzem um valor que você usa em outro lugar. Um comum é if:

// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0

let addOneIfOdd input =
    let result =
        if isOdd input then
            input + 1
        else
            input

    result

A if expressão produz um valor chamado result. Observe que você pode omitir result completamente, tornando a if expressão o corpo da addOneIfOdd função. A principal coisa a se lembrar das expressões é que elas produzem um valor.

Há um tipo especial, unitque é usado quando não há nada a ser retornado. Por exemplo, considere esta função simples:

let printString (str: string) =
    printfn $"String is: {str}"

A assinatura tem esta aparência:

val printString: str:string -> unit

O unit tipo indica que não há nenhum valor real sendo retornado. Isso é útil quando você tem uma rotina que deve "trabalhar" apesar de não ter nenhum valor para retornar como resultado desse trabalho.

Isso contrasta fortemente com a programação imperativa, em que o constructo equivalente if é uma instrução e a produção de valores geralmente é feita com variáveis de mutação. Por exemplo, em C#, o código pode ser escrito da seguinte maneira:

bool IsOdd(int x) => x % 2 != 0;

int AddOneIfOdd(int input)
{
    var result = input;

    if (IsOdd(input))
    {
        result = input + 1;
    }

    return result;
}

Vale a pena observar que c# e outras linguagens de estilo C dão suporte à expressão ternária, que permite programação condicional baseada em expressão.

Na programação funcional, é raro alterar valores com instruções. Embora algumas linguagens funcionais ofereçam suporte a instruções e mutações, não é comum usar esses conceitos na programação funcional.

Funções puras

Como mencionado anteriormente, funções puras são funções que:

  • Sempre avalie o mesmo valor para uma mesma entrada.
  • Não tem efeitos colaterais.

É útil pensar em funções matemáticas nesse contexto. Em matemática, as funções dependem apenas de seus argumentos e não têm efeitos colaterais. Na função matemática f(x) = x + 1, o valor de f(x) depende apenas do valor de x. Funções puras na programação funcional são da mesma maneira.

Ao escrever uma função pura, a função deve depender apenas de seus argumentos e não executar nenhuma ação que resulte em um efeito colateral.

Aqui está um exemplo de uma função não pura porque depende do estado global mutável:

let mutable value = 1

let addOneToValue x = x + value

A addOneToValue função é claramente impure, pois value pode ser alterada a qualquer momento para ter um valor diferente de 1. Esse padrão de depender de um valor global deve ser evitado na programação funcional.

Aqui está outro exemplo de uma função não pura, porque ela executa um efeito colateral:

let addOneToValue x =
    printfn $"x is %d{x}"
    x + 1

Embora essa função não dependa de um valor global, ela grava o valor de x na saída do programa. Embora não haja nada inerentemente errado em fazer isso, isso significa que a função não é pura. Se outra parte do programa depender de algo externo ao programa, como o buffer de saída, chamar essa função poderá afetar essa outra parte do programa.

Remover a printfn instrução torna a função pura:

let addOneToValue x = x + 1

Embora essa função não seja inerentemente melhor do que a versão anterior com a printfn instrução, ela garante que toda essa função retorne um valor. Chamar essa função várias vezes produz o mesmo resultado: ela apenas produz um valor. A previsibilidade dada pela puridade é algo pelo qual muitos programadores funcionais se esforçam.

Imutabilidade

Por fim, um dos conceitos mais fundamentais da programação funcional tipada é a imutabilidade. Em F#, todos os valores são imutáveis por padrão. Isso significa que eles não podem ser modificados no local, a menos que você os marque explicitamente como mutáveis.

Na prática, trabalhar com valores imutáveis significa que você altera sua abordagem de programação de"Preciso mudar algo" para "Preciso produzir um novo valor".

Por exemplo, adicionar 1 a um valor significa produzir um novo valor, não alterar o existente:

let value = 1
let secondValue = value + 1

Em F#, o código a seguir não altera a value função; em vez disso, ele executa uma verificação de igualdade:

let value = 1
value = value + 1 // Produces a 'bool' value!

Algumas linguagens de programação funcionais não dão suporte a mutações. No F#, há suporte, mas não é o comportamento padrão para valores.

Esse conceito se estende ainda mais às estruturas de dados. Na programação funcional, estruturas de dados imutáveis, como conjuntos (e muitos mais), têm uma implementação diferente do esperado inicialmente. Conceitualmente, algo como adicionar um item a um conjunto não altera o conjunto, ele produz um novo conjunto com o valor agregado. Por trás dos panos, isso geralmente é realizado por uma estrutura de dados diferente que permite o acompanhamento eficiente de um valor, garantindo que a representação apropriada dos dados possa ser fornecida como resultado.

Esse estilo de trabalhar com valores e estruturas de dados é fundamental, pois força você a tratar qualquer operação que modifique algo como se criasse uma nova versão dessa coisa. Isso permite que coisas como igualdade e comparabilidade sejam consistentes em seus programas.

Próximas etapas

A próxima seção abordará completamente as funções, explorando diferentes maneiras de usá-las na programação funcional.

O uso de funções em F# explora profundamente as funções, mostrando como você pode usá-las em vários contextos.

Leitura adicional

A série Thinking Functionally é outro ótimo recurso para aprender sobre programação funcional com F#. Aborda os conceitos básicos da programação funcional de forma pragmática e fácil de ler, usando recursos F# para ilustrar os conceitos.