Introdução aos Conceitos de Programação Funcional em F#

Programação funcional é um estilo de programação que enfatiza o uso de funções e dados imutáveis. 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 como as construções primárias que você usa
  • Expressões em vez de instruções
  • Valores imutáveis sobre variáveis
  • Programação declarativa sobre programação imperativa

Ao longo desta série, você explorará conceitos e padrões em programação funcional usando F#. Ao longo do caminho, você aprenderá alguns 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 dada uma entrada. Mais formalmente, ele mapeia um item de um conjunto para outro. Este formalismo é levado ao concreto de muitas maneiras, especialmente quando se utilizam funções que operam em coleções de dados. É o conceito mais básico (e importante) em programação funcional.
  • Expressão - Uma expressão é uma construção em código que produz um valor. Em F#, esse valor deve ser vinculado ou explicitamente ignorado. Uma expressão pode ser substituída trivialmente por uma chamada de função.
  • Pureza - A pureza é uma propriedade de uma função tal que o seu valor de retorno é sempre o mesmo para os mesmos argumentos, e que a sua avaliação não tem efeitos secundários. Uma função pura depende inteiramente dos seus argumentos.
  • Transparência Referencial - A Transparência Referencial é uma propriedade de expressões tal que elas podem 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 as variáveis, que podem mudar de lugar.

Exemplos

Os exemplos a seguir demonstram esses conceitos centrais.

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

A sua assinatura de tipo é a seguinte:

val addOne: x:int -> int

A assinatura pode ser lida como, "addOne aceita um int nome 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 olhar para 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 digitada, a implementação de uma função é muitas vezes menos importante do que a assinatura de tipo real! O fato de que addOne adiciona o valor 1 a um inteiro é interessante em tempo de execução, mas quando você está construindo um programa, o fato de que ele aceita e retorna um int é o que informa como você realmente usará essa função. Além disso, uma vez que você usa esta função corretamente (com relação à sua assinatura de tipo), o diagnóstico de quaisquer problemas pode ser feito apenas dentro do corpo da addOne função. Este é o impulso por trás da programação funcional digitada.

Expressões

As expressões são construções que avaliam um valor. Em contraste com as declaraçõ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 em 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 desta 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() sido chamado, o tipo de x foi tornado genérico (chamado de Generalização Automática), e o tipo resultante é um string.

As expressões não são apenas os corpos de 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 lembrar sobre as expressões é que elas produzem um valor.

Há um tipo especial, unitque é usado quando não há nada para retornar. 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 "fazer trabalho" apesar de não ter valor para retornar como resultado desse trabalho.

Isto está em nítido contraste com a programação imperativa, onde o constructo equivalente if é uma declaração, e a produção de valores é frequentemente feita com variáveis mutantes. Por exemplo, em C#, o código pode ser escrito assim:

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 notar que C# e outras linguagens de estilo C suportam a expressão ternária, que permite a programação condicional baseada em expressão.

Na programação funcional, é raro mutar valores com instruções. Embora algumas linguagens funcionais suportem declaraçõ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:

  • Avalie sempre com o mesmo valor para a mesma entrada.
  • Não tem efeitos secundários.

É útil pensar em funções matemáticas neste contexto. Em matemática, as funções dependem apenas dos seus argumentos e não têm quaisquer efeitos secundários. Na função f(x) = x + 1matemática , o valor de f(x) depende apenas do valor de x. Funções puras em 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 qualquer 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 impura, porque value pode ser alterada a qualquer momento para ter um valor diferente de 1. Este padrão de dependência de um valor global deve ser evitado na programação funcional.

Aqui está outro exemplo de uma função não pura, porque 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 para a 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 pode 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 tudo o que essa função faz é retornar um valor. Chamar essa função qualquer número de vezes produz o mesmo resultado: apenas produz um valor. A previsibilidade dada pela pureza é algo que muitos programadores funcionais almejam.

Imutabilidade

Finalmente, 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 mutados no local, a menos que você os marque explicitamente como mutáveis.

Na prática, trabalhar com valores imutáveis significa que você muda 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 mutar o existente:

let value = 1
let secondValue = value + 1

Em F#, o código a seguir não muta 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 suportam mutação. Em F#, ele é suportado, mas não é o comportamento padrão para valores.

Este conceito estende-se ainda mais às estruturas de dados. Na programação funcional, estruturas de dados imutáveis, como conjuntos (e muito mais), têm uma implementação diferente do que você poderia esperar inicialmente. Conceitualmente, algo como adicionar um item a um conjunto não altera o conjunto, ele produz um novo conjunto com o valor agregado. Sob as cobertas, isso geralmente é realizado por uma estrutura de dados diferente que permite rastrear eficientemente um valor para que a representação apropriada dos dados possa ser dada 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óximos passos

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

O uso de funções em F# explora funções profundamente, 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#. Abrange fundamentos de programação funcional de forma pragmática e de fácil leitura, usando recursos de F# para ilustrar os conceitos.