A terceira parte de uma série de artigos sobre programação funcional chegou. Hoje falaremos sobre todos os tipos desse paradigma e mostraremos exemplos de seu uso. Mais informações sobre tipos primitivos, tipos generalizados e muito mais sobre o corte!

Agora que temos alguma compreensão das funções, veremos como os tipos interagem com funções como domínio e intervalo. Este artigo é apenas uma revisão. Para uma imersão mais profunda nos tipos, há uma série de "entendimento dos tipos de F #" .
Para começar, precisamos entender um pouco melhor a notação de tipo. Vimos a notação de seta " ->
" separando domínio e intervalo. Portanto, a assinatura da função sempre se parece com isso:
val functionName : domain -> range
Mais alguns exemplos de funções:
let intToString x = sprintf "x is %i" x // int string let stringToInt x = System.Int32.Parse(x)
Se você executar esse código em uma janela interativa , poderá ver as seguintes assinaturas:
val intToString : int -> string val stringToInt : string -> int
Eles significam:
intToString
possui um domínio do tipo int
, que é mapeado para o intervalo do tipo string
.stringToInt
possui um domínio do tipo string
, que é mapeado para um intervalo do tipo int
.
Tipos primitivos
Existem tipos primitivos esperados: string, int, float, bool, char, byte, etc., bem como muitos outros derivados do sistema de tipos .NET.
Mais alguns exemplos de funções com tipos primitivos:
let intToFloat x = float x // "float" - int float let intToBool x = (x = 2) // true x 2 let stringToString x = x + " world"
e suas assinaturas:
val intToFloat : int -> float val intToBool : int -> bool val stringToString : string -> string
Anotação de tipo
Nos exemplos anteriores, o compilador F # definiu corretamente os tipos de parâmetros e resultados. Mas isso nem sempre acontece. Se você tentar executar o seguinte código, você receberá um erro de compilação:
let stringLength x = x.Length => error FS0072: Lookup on object of indeterminate type
O compilador não sabe o tipo do argumento "x" e, por isso, não sabe se o "Comprimento" é um método válido. Na maioria dos casos, isso pode ser corrigido passando a "anotação de tipo" para o compilador F #. Então ele saberá que tipo usar. Na versão fixa, indicamos que o tipo "x" é string.
let stringLength (x:string) = x.Length
As chaves ao redor do parâmetro x:string
são importantes. Se eles forem ignorados, o compilador decidirá que a string é o valor de retorno! Ou seja, dois pontos abertos são usados para indicar o tipo de valor de retorno, conforme mostrado no exemplo a seguir.
let stringLengthAsInt (x:string) :int = x.Length
Indicamos que o parâmetro x
é uma string e o valor de retorno é um número inteiro.
Tipos de funções como parâmetros
Uma função que assume outras funções como parâmetros ou retorna uma função é chamada de função de ordem superior (a função de ordem superior às vezes é reduzida para HOF). Eles são usados como uma abstração para definir um comportamento o mais geral possível. Esse tipo de função é muito comum em F #, a maioria das bibliotecas padrão as utiliza.
Considere a função evalWith5ThenAdd2
, que aceita uma função como parâmetro e calcula essa função de 5 e adiciona 2 ao resultado:
let evalWith5ThenAdd2 fn = fn 5 + 2 // , fn(5) + 2
A assinatura desta função tem a seguinte aparência:
val evalWith5ThenAdd2 : (int -> int) -> int
Você pode ver que o domínio é (int->int)
e o intervalo é int
. O que isso significa? Isso significa que o parâmetro de entrada não é um valor simples, mas uma função de muitas funções, de int
para int
. O valor de saída não é uma função, mas apenas um int
.
Vamos tentar:
let add1 x = x + 1 // - (int -> int) evalWith5ThenAdd2 add1 //
e obtenha:
val add1 : int -> int val it : int = 8
" add1
" é uma função que mapeia int
a int
, como vemos na assinatura. É um parâmetro válido para evalWith5ThenAdd2
e seu resultado é 8.
A propósito, a palavra especial " it
" é usada para indicar o último valor calculado, neste caso, é o resultado que estávamos esperando. Esta não é uma palavra-chave, é apenas uma convenção de nomenclatura.
Outro caso:
let times3 x = x * 3 // - (int -> int) evalWith5ThenAdd2 times3 //
dá:
val times3 : int -> int val it : int = 17
" times3
" também é uma função que mapeia int
a int
, como pode ser visto na assinatura. Também é um parâmetro válido para evalWith5ThenAdd2
. O resultado dos cálculos é 17.
Observe que os dados de entrada são sensíveis ao tipo. Se a função passada usa um float
, não um int
, nada funcionará. Por exemplo, se tivermos:
let times3float x = x * 3.0 // - (float->float) evalWith5ThenAdd2 times3float
O compilador, ao tentar compilar, retornará um erro:
error FS0001: Type mismatch. Expecting a int -> int but given a float -> float
relatando que a função de entrada deve ser uma função do tipo int->int
.
Funções como Saída
Funções de valor também podem ser o resultado de funções. Por exemplo, a função a seguir gerará uma função "somador" que adicionará um valor de entrada.
let adderGenerator numberToAdd = (+) numberToAdd
A assinatura dela:
val adderGenerator : int -> (int -> int)
significa que o gerador pega um int
e cria uma função ("somador") que mapeia ints
para ints
. Vamos ver como funciona:
let add1 = adderGenerator 1 let add2 = adderGenerator 2
Duas funções adicionadoras são criadas. O primeiro cria uma função que adiciona 1 à entrada, o segundo adiciona 2. Observe que as assinaturas são exatamente o que esperávamos.
val add1 : (int -> int) val add2 : (int -> int)
Agora você pode usar as funções geradas como de costume, elas não são diferentes das funções definidas explicitamente:
add1 5 // val it : int = 6 add2 5 // val it : int = 7
Usando anotações de tipo para restringir tipos de função
No primeiro exemplo, vimos uma função:
let evalWith5ThenAdd2 fn = fn 5 +2 > val evalWith5ThenAdd2 : (int -> int) -> int
Neste exemplo, o F # pode concluir que " fn
" converte int
em int
, portanto sua assinatura será int->int
.
Mas qual é a assinatura de "fn" no seguinte caso?
let evalWith5 fn = fn 5
É claro que " fn
" é um tipo de função que recebe um int
, mas o que ele retorna? O compilador não pode responder a esta pergunta. Nesses casos, se for necessário indicar o tipo de função, você poderá adicionar um tipo de anotação para os parâmetros da função e também para os tipos primitivos.
let evalWith5AsInt (fn:int->int) = fn 5 let evalWith5AsFloat (fn:int->float) = fn 5
Além disso, você pode determinar o tipo de retorno.
let evalWith5AsString fn :string = fn 5
Porque a função principal retorna string
, a função " fn
" também é forçada a retornar string
. Portanto, não é necessário especificar explicitamente o tipo " fn
".
Digite "unidade"
No processo de programação, às vezes queremos que uma função faça algo sem retornar nada. Considere a função " printInt
". A função realmente não retorna nada. Simplesmente imprime a string no console como um efeito colateral da execução.
let printInt x = printf "x is %i" x //
Qual é a assinatura dela?
val printInt : int -> unit
O que é uma " unit
"?
Mesmo que a função não retorne valores, ela ainda precisará de um intervalo. Não existem funções "vazias" no mundo da matemática. Cada função deve retornar algo, porque a função é um mapeamento e o mapeamento deve exibir algo!

Portanto, em F #, funções como essa retornam um tipo especial de resultado chamado " unit
". Ele contém apenas um valor, indicado por " ()
". Você pode pensar que unit
e ()
são algo como "void" e "null" do C #, respectivamente. Mas, diferentemente deles, a unit
é do tipo real e ()
valor real. Para verificar isso, basta:
let whatIsThis = ()
A seguinte assinatura será recebida:
val whatIsThis : unit = ()
O que indica que o rótulo " whatIsThis
" é do tipo unit
e está associado a um valor ()
.
Agora, retornando à assinatura " printInt
", podemos entender o significado dessa entrada:
val printInt : int -> unit
Essa assinatura diz que printInt
tem um domínio int
, que se traduz em algo que não nos interessa.
Funções sem parâmetros
Agora que entendemos a unit
, podemos prever sua aparência em um contexto diferente? Por exemplo, tente criar uma função reutilizável "olá mundo". Como não há entrada ou saída, podemos esperar a unit -> unit
assinatura unit -> unit
. Vamos ver:
let printHello = printf "hello world" //
Resultado:
hello world val printHello : unit = ()
Não é exatamente o que esperávamos. "Hello world" foi emitido imediatamente e o resultado não foi uma função, mas um simples valor do tipo unit. Podemos dizer que esse é um valor simples, porque, como vimos anteriormente, ele possui uma assinatura do formulário:
val aName: type = constant
Neste exemplo, vemos que printHello
realmente um valor simples ()
. Esta não é uma função que podemos chamar mais tarde.
Qual é a diferença entre printInt
e printHello
? No caso de printInt
valor não pode ser determinado até conhecermos o valor do parâmetro x
, portanto, a definição era uma função. No caso de printHello
não há parâmetros, portanto, o lado direito pode ser definido no local. E foi igual a ()
com um efeito colateral na forma de saída para o console.
Você pode criar uma verdadeira função reutilizável sem parâmetros, forçando a definição a ter um argumento de unit
:
let printHelloFn () = printf "hello world" //
Agora a assinatura dela é igual a:
val printHelloFn : unit -> unit
e para chamá-lo, devemos passar ()
como um parâmetro:
printHelloFn ()
Fortalecer tipos de unidades com a função ignorar
Em alguns casos, o compilador requer um tipo de unit
e reclama. Por exemplo, os dois casos a seguir causarão um erro do compilador:
do 1+1 // => FS0020: This expression should have type 'unit' let something = 2+2 // => FS0020: This expression should have type 'unit' "hello"
Para ajudar nessas situações, existe uma função especial de ignore
que aceita qualquer coisa e retorna a unit
. A versão correta desse código pode ser a seguinte:
do (1+1 |> ignore) // ok let something = 2+2 |> ignore // ok "hello"
Tipos genéricos
Na maioria dos casos, se o tipo de um parâmetro de função puder ser de qualquer tipo, precisamos dizer algo sobre isso. O F # usa genéricos do .NET para essas situações.
Por exemplo, a função a seguir converte um parâmetro em uma string adicionando algum texto:
let onAStick x = x.ToString() + " on a stick"
Independentemente do tipo de parâmetro, todos os objetos podem fazer em ToString()
.
Assinatura:
val onAStick : 'a -> string
Que tipo 'a
? No F #, é uma maneira de indicar um tipo genérico desconhecido no tempo de compilação. Um apóstrofo antes de "a" significa que o tipo é genérico. Equivalente a esta assinatura em C #:
string onAStick<a>(); // string OnAStick<TObject>(); // F#- 'a // C#'- "TObject"
Deve-se entender que essa função F # ainda possui digitação forte, mesmo com tipos genéricos. Ele não aceita um parâmetro do tipo Object
. A digitação forte é boa porque permite manter a segurança do tipo ao compor funções.
A mesma função é usada para int
, float
e string
.
onAStick 22 onAStick 3.14159 onAStick "hello"
Se houver dois parâmetros generalizados, o compilador fornecerá dois nomes diferentes: 'a
para o primeiro 'b
, 'b
para o segundo, etc. Por exemplo:
let concatString xy = x.ToString() + y.ToString()
Haverá dois tipos genéricos nessa assinatura: 'a
e 'b
:
val concatString : 'a -> 'b -> string
Por outro lado, o compilador reconhece quando apenas um tipo genérico é necessário. No exemplo a seguir, x
e y
devem ser do mesmo tipo:
let isEqual xy = (x=y)
Portanto, uma assinatura de função tem o mesmo tipo genérico para os dois parâmetros:
val isEqual : 'a -> 'a -> bool
Parâmetros generalizados também são muito importantes quando se trata de listas e outras estruturas abstratas, e veremos muitos deles nos exemplos a seguir.
Outros tipos
Até agora, apenas os tipos básicos foram discutidos. Esses tipos podem ser combinados de várias maneiras em tipos mais complexos. Sua análise completa será posteriormente em outra série , mas, enquanto isso, e aqui vamos analisá-las brevemente, para que você possa reconhecê-las nas assinaturas de funções.
- Tuplas Este é um par, um triplo, etc., composto de outros tipos. Por exemplo,
("hello", 1)
é uma tupla baseada em string
e int
. Uma vírgula é uma marca registrada das tuplas; se uma vírgula é vista em algum lugar no F #, é quase garantido que isso faz parte da tupla.
Nas assinaturas de funções, as tuplas são gravadas como "produtos" dos dois tipos envolvidos. Nesse caso, a tupla será do tipo:
string * int // ("hello", 1)
- Coleções . Os mais comuns são lista (lista), seq (sequência) e matriz. As listas e matrizes têm tamanho fixo, enquanto as seqüências são potencialmente infinitas (nos bastidores, as sequências são as mesmas
IEnumrable
). Nas assinaturas de funções, eles têm suas próprias palavras-chave: " list
", " seq
" e " []
" para matrizes.
int list // List type [1;2;3] string list // List type ["a";"b";"c"] seq<int> // Seq type seq{1..10} int [] // Array type [|1;2;3|]
- Opção (tipo opcional) . Este é um invólucro simples sobre objetos que podem estar ausentes. Existem duas opções:
Some
(quando o valor existe) e None
(quando o valor não existe). Nas assinaturas de função, eles têm sua própria palavra-chave " option
":
int option // Some 1
- A associação marcada (união discriminada) . Eles são construídos a partir de muitas variações de outros tipos. Vimos alguns exemplos em "por que usar o F #?" . Nas assinaturas de funções, elas são referenciadas pelo nome do tipo e não possuem uma palavra-chave especial.
- Tipo de registro (registros) . Tipos como estruturas ou linhas de banco de dados, um conjunto de valores nomeados. Também vimos alguns exemplos em "por que usar F #?" . Nas assinaturas de função, elas são chamadas pelo nome do tipo e também não possuem sua própria palavra-chave.
Teste sua compreensão dos tipos
Aqui estão algumas expressões para testar sua compreensão das assinaturas de funções. Para verificar, basta executá-los em uma janela interativa!
let testA = float 2 let testB x = float 2 let testC x = float 2 + x let testD x = x.ToString().Length let testE (x:float) = x.ToString().Length let testF x = printfn "%s" x let testG x = printfn "%f" x let testH = 2 * 2 |> ignore let testI x = 2 * 2 |> ignore let testJ (x:int) = 2 * 2 |> ignore let testK = "hello" let testL() = "hello" let testM x = x=x let testN x = x 1 // : x? let testO x:string = x 1 // : :string ?
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.