Pensamento funcional. Parte 3

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.

Source: https://habr.com/ru/post/pt422115/


All Articles