Fundamentos Teóricos da Linguagem F# - PDF
Fundamentos Teóricos da Linguagem F#
Download "Fundamentos Teóricos da Linguagem F#"
Martín Peixoto Paiva
1 Fundamentos Teóricos da Linguagem F# Instituto de Informática Universidade Federal do Rio Grande do Sul (UFRGS) Caixa Postal Porto Alegre RS Brasil Pedro Dusso, Rafael Scortegagna, Luiza de Souza & Otávio Zabaleta pmdusso, rscortegagna, lsouza, Conteúdo Introdução... 2 Whitespaces Matthers... 2.NET Interop... 3 Fundamentos... 4 Tipos Primitivos... 4 Comparação Igualdade... 4 Funções... 5 Type Inference... 5 Generic Functions... 6 Escopo... 6 Fluxo de Controle... 7 Core Types... 9 Unit... 9 Tupla Listas Operadores de agregação (aggregate operators) Option Anatomia de um Programa F Sharp Módulos Namespaces Program Startup Exemplos Práticos Conclusões Bibliografia... 27
2 Introdução A programação funcional (FP) é a melhor abordagem para resolver muitos difíceis problemas computacionais. Entretanto, a FP pura não é adequada para programação de propósitos gerais. Por isso, as FP vem, ao longo dos anos, adicionando aspectos de paradigmas imperativos e orientado a objetos, permanecendo linguagens funcionais mas incorporando características necessárias para escrever facilmente qualquer tipo de programa. F# é um sucessor natural nesse caminho, ela é muito mais do que apenas uma linguagem funcional. Algumas das linguagens funcionais mais populares, como OCaml, Haskell, Lisp e Scheme, tradicionalmente implementaram seus próprios runtimes, o que levou a problemas de interoperabilidade. F#, como uma linguagem de programação de propósito geral para o.net (um runtime de propósito geral), suavemente integra os três os três maiores paradigmas de programação. Com o F# é possível escolher o paradigma que melhor funciona para um dado problema, da maneira mais eficiente possível. Se você é um purista funcional, pode programar de maneira pura; se não, pode facilmente combinar programação funcional, imperativa e orientada a objetos no mesmo programa explorando as vantagens de cada paradigma. Assim como em outras linguagens funcionais, F# é fortemente tipada, contudo, como também usa inferência de tipos, os programadores não precisam perder tempo explicitando tipos a não ser que uma ambigüidade exista. Além disso, F# integra-se facilmente com o.net Framwork Base Class Library (BCL). Usar o BCL com o F# é tão fácil quanto usá-lo com o C# ou o VB e talvez mais fácil. F# foi modelada sobre OCaml, uma FP orientada a objetos bem sucedida, e, então, refinada e estendida para se entrosar com a técnica e a filosofia do.net. Ela implementa completamente o framework, permitindo aos usuários fazer qualquer coisa que ele possibilite. O compilador F# pode compilar para qualquer implementação do Common Language Infrastructure 1 (CLI) e suporta os tipos genéricos do.net sem qualquer alteração de código. O compilador F# não só produz executáveis para qualquer CLI, mas também pode rodar em qualquer ambiente que tenha um CLI, o que significa que o F# não está limitado ao Windows podendo rodar em Linux, Apple Mac OS X e OpenBSD. O F# foi primeiro implementado pelo Dr. Don Syme no Microsoft Reasearch (MSR) em Cambrige. O projeto agora foi abraçado pela Microsoft Corporate em Redmond, e a implementação do compilador e a integração com o Visual Studio agora é desenvolvida em ambos os lugares. Whitespaces Matthers Linguagens como C# usam ponto e vírgula (;) e chaves () para indicar quando blocos de código estão completos. Programadores normalmente identam seus códigos para torná-los mais legíveis, logo esses símbolos extras acabam apenas inchando o código. Em F#, espaços em branco e novas linhas são significantes. Por exemplo, qualquer código identado além de um IF está contido no escopo deste IF. A regra geral é que nada que pertence a um método ou a uma expressão deve ser identado além ta palavra chave que inicia o método ou a expressão. E.g., se o corpo da expressão IF for identado com quatro espaços e depois finalizado com a palavra chave IF, o compilador F# vai acusar um warning. 1 A Common Language Infrastructure ou CLI é uma especificação aberta (ECMA-335 e ISO/IEC 23271) desenvolvida pela Microsoft que descreve o código executável e ambiente runtime que forma o core da Microsoft.NET Framework e das implementações Mono e Portable.NET.A especificação define um ambiente que permite a utilização de múltiplas linguagens de alto nível em diferentes plataformas sem a necessidade de serem reescritas para uma arquitetura específica.
3 let main (args : string[]) = if args.length <> 2 then failwith "Error: Expected arguments <greeting> and <thing>" let greeting, thing = args.[0], args.[1] let timeofday = DateTime.Now.ToString("hh:mm tt") printfn "%s, %s at %s" greeting thing timeofday //Program exit code 0 [<EntryPoint>] let main (args : string[]) = if args.length <> 2 then failwith "Error: Expected arguments <greeting> and <thing>" Warning FS0058: possible incorrect indentation: this token is offside of context started at position (25:5). Try indenting this token further or using standard formatting conventions.net Interop O framework.net contém uma gama muito grande de bibliotecas, desde gráficos até banco de dados e WebServices. A linguagem F# pode tirar vantagens de qualquer biblioteca.net nativamente. Como qualquer linguagem, F# permite ao usuário comentar seu código. Para comentar uma linha só são usados //. Se for desejado comentar mais de uma linha pode-se usar (* e *), onde (* é onde se iniciam os comentários e *) é onde finaliza o código comentado. Em aplicações F# implementadas no Visual Studio, podemos comentar o código de uma maneira diferente, usando uma documentação XML para comentários. Se o comentário começar com três barras (///), o Visual Studio vai mostrar o comentário quando o mouse estiver sobre a função.
4 Fundamentos Tipos Primitivos Um tipo é um conceito (ou uma abstração) o qual seu principal objetivo é forçar a segurança dos dados. Alguns tipos são diretos como um inteiro enquanto outros são mais abstratos como uma função. F# é estaticamente tipada, i.e., a checagem de tipos é feita em tempo de compilação. > let intfunc x y = x + 2*y;; val intfunc : int -> int -> int > intfunc "one" "two";; intfunc "one" "two";; ^^^^^ stdin(4,9): error FS0001: This expression was expected to have type int but here has type string Tipos numéricos: byte, sbyte, int16, uint16, int, int32, uint, uint32, int64, uint64, float, float32, decimal.é possivel usar os operadores aritméticos padrões nos tipos numéricos primitivos, sendo eles: + adição, - subtração, * multiplicação, / divisão, ** potência e % modulo. A plataforma.net é baseada em Unicode, sendo assim os caracteres são representados usando dois bytes UTF-16. Characters são escritos entre aspas simples: a, b, c... Os strings, por sua vez, são definidos entre aspas duplas, que podem ser definidos em múltiplas linhas. É possível acessar um caractere dentro de um string acessando diretamente seu índice por s- tring.[indice]. Por exemplo, abracadabra. Os booleanos, em F#, podem ser apenas true ou false. Os operadores sobre booleans são o and (&&), o or ( ) e a negação (not). Comparação Igualdade É possível comparar valores numéricos usando maior que, menor que e operadores de igualdade: Operador Descrição Exemplo Resultado < Menor que 1 < 2 True <= Menor ou igual que 4.0 <= 4.0 True > Maior que 1.4e3 > 1.0e2 True >= Maior ou igual que 0I >= 2I False = Igual a abc = abc True <> Não igual a (diferente) a <> b True compare Comparação entre dois valores compare
5 Todos os operadores de igualdade avaliam a expressão para um valor booleano. A função compare retorna -1, 0 ou 1, dependendo se o primeiro parâmetro for menor, igual ou maior que o segundo. Ela é extremamente útil quando é necessário implementar comparações entre tipos abstratos de dados, para realizar ordenações customizadas. Vale ressaltar que igualdade é um tópico delicado no framework.net. Existe a igualdade de valores e igualdade de referências. Para tipos valores, a comparação significa valores idênticos, e.g. 1 =1. Para tipos de referência, entretanto, a igualdade é determinada fazendo overriding do método Equals do System.Object. Funções Em F#, as funções são definidas utilizando a palavra fun. Os argumentos de uma função são separados por espaços, enquanto o seu corpo é separado através de uma seta para esquerda. fun x y -> x + y É importante observar que a função definida anteriormente não tem um nome. Funções definidas dessa maneira são chamadas de funções anônimas ( anonymous functions ), funções lambda ( lambda functions ), ou apenas lambdas. Esse tipo de função normalmente é utilizado como argumento de outra função. Para dar nome aos identificadores é necessário utilizar a palavra let seguida do nome do identificador, de um sinal de igual e da expressão que ela referencia. Por exemplo, para associar um valor a um identificador seria necessário fornecer o seguinte comando. let x = 42 Uma vez associado um valor a um identificador, ele não é alterado. Isso faz com que ele difere das variáveis existentes na linguagem imperativa. Os identificadores servem tanto para referenciar valores quanto funções, conforme o mostrado a baixo. let myadd = fun x y -> x + y Entretanto, a linguagem F# fornece outra sintaxe curta e específica para a definição de funções, como o apresentado abaixo. let myadd x y = x + y Há algumas regras associadas aos nomes dos identificadores. Eles devem iniciar com uma letra ou com um underscore, em seguida podem conter qualquer caractere alfanumérico, underscore ou aspas simples. É importante observar que F# não tem nenhuma palavra-chave return. Logo, ao se definir uma função, a sua última expressão a ser avaliada é o que a função retornará. Type Inference O compilador F# não exige que os tipos de todos os parâmetros sejam explicitamente declarados. O compilador infere os tipos baseado no uso dos parâmetros.
6 let addone x = x + 1;; > val addone : int -> int > let mult x y = x * y > let result = mult ;; val mult : float -> float -> float val result : float = 20.0 Entretanto, o programador pode fornecer uma dica de tipo (type annotation) para o compilador: > let add (x : float) y = x + y;; val add : float -> float -> float A inferência de tipos reduz drasticamente a confusão de declaração de tipos no código, deixando para o compilador a tarefa de saber qual o tipo específico que deve ser usado. Ocasionalmente, uma dica de tipo é necessária e pode inclusive ajudar a legibilidade do código. Generic Functions É possível escrever funções que funcionam para qualquer tipo de parâmetros. A identidade de uma função, a qual retorna apenas o parâmetro que foi fornecido, é um exemplo disso. > let ident x = x;; val ident : 'a -> 'a Como o sistema de inferência de tipos não pode determinar um tipo para o valor x na função ident, ele ficou genérico. Se um parâmetro é genérico isso significa que aquele parâmetro pode assumir qualquer tipo integer, float, string... O tipo de um parâmetro genérico pode ter o nome de um identificador válido iniciado por uma a- póstrofe. O trecho de código a seguir redefine a função ident forçando o parâmetro x a ser genérico. > let ident2 (x : 'a) = x;; val ident2 : 'a -> 'a Escopo Todo valor tem um escopo específico, o qual é a série de locais onde o valor pode ser usado. Por default, os valores podem ser usados em qualquer lugar após a sua declaração. No entanto, se eles forem definidos dentro de uma função, eles podem ser usados apenas para essa função. O exemplo a seguir mostra exatamente isso. > // Scope let modulevalue = 10 let functiona x = x + modulevalue;; val modulevalue : int = 10
7 val functiona : int -> int > // Error case let functionb x = let functionvalue = 20 x + functionvalue // 'functionvalue' not in scope functionvalue;; functionvalue;; ^^^^^^^^^^^^^ error FS0039: The value or constructor 'functionvalue' is not defined. Funções aninhadas são capazes de acessar um valor declarado em um escopo anterior, como o da função pai ou do seu próprio módulo, assim como quaisquer valores declarados dentro do seu escopo. O trecho de código a seguir mostra exatamente esse funcionamento. > // Nested functions let modulevalue = 1 let f fparam = let g gparam = fparam + gparam + modulevalue let a = g 1 let b = g 2 a + b;; val modulevalue : int = 1 val f : int -> int Como podemos perceber, a função g é capaz de acessar o parâmetro fparam definido no escopo da função anterior. Em F# é possível ter dois valores diferentes para um mesmo nome. Quando isso ocorre, o último a ser declarado é o válido, apesar dos dois existirem na memória. Fluxo de Controle IF expression: retorna um valor. Por esse motivo, a linguagem F# se difere das outras linguagens. Como o resultado da expressão IF retorna um valor, todas as cláusulas de uma expressão IF devem ser do mesmo tipo. > let x = if 1 > 2 then 42 else "a string";;
8 "a string" ^^^^^^^^^^ > stdin(27,9): error FS0001: This expression was expected to have type int but here has type string Existe uma facilidade (açúcar sintático) para ajudar os programadores a combaterem Ifs encadeados muito longos, é a palavra elif: let isweekend day = if day = "Sunday" then true else if day = "Saturday" then true else false... let isweekend day = if day = "Sunday" then true elif day = "Tuesday" then true elif day = "Wednesday" then true elif day = "Thursday" then true elif day = "Friday" then true else false Como o resultado de uma if expression é um valor, é necessário que todos eles retornem o mesmo tipo de dado. O trecho de código abaixo mostra exatamente isso. > // ERROR: Different types for if expression clauses let x = if 1 > 2 then 42 else "a string";; else "a string";; ^^^^^^^^^^^ stdin(118,19): error FS0001: This expression has type string but is here used with type int. stopped due to error Caso haja apenas um if sem um else correspondente, então é necessário que essa cláusula retorne uma unit. Em F#, uma unit é um tipo especial, que indica sem valor ( no value ). O exemplo a seguir funciona apenas pois o printf retorna uma unit. > // If statement bodies returning unit let describenumber x = if x % 2 = 0 then printfn "x is a multiple of 2" if x % 3 = 0 then printfn "x is a multiple of 3" if x % 5 = 0 then
9 printfn "x is a multiple of 5" ();; val describenumber : int -> unit > describenumber 18;; x is a multiple of 2 x is a multiple of 3 val it : unit = () Core Types Existem vários core types que ajudam o programador a organizar, manipular e processar dados. A tabela abaixo apresenta os principais core types da linguagem F#. Assinatura Nome Definição Exemplo Unit Unit sem valor () int, float Tipo Concreto um tipo concreto 42, 3.15 'a, 'b Tipo Genérico um tipo genérico (livre) 'a -> 'b Tipo Função uma função que retorna um valor fun x -> x + 1 'a * 'b Tipo Tupla uma coleção ordenada de valores (1, 2), ( cachorro, gato ) 'a list Tipo Lista uma lista de valores [1; 2; 3], [1.. 3] 'a option Tipo Opcional um valor opcional Some(3), None Unit O tipo unit é um valor que não tem nenhuma consequência associada. Ela pode ser considerada como uma representação concreta de uma void e é representada por (). A seguir, são apresentados alguns exemplos da utilização da unit. > let x = ();; val x : unit > ();; val it : unit = () Em F#, toda a função precisa retornar um valor, mas caso a função não retorne conceitualmente nenhum nada, ela deve retornar uma unit. A função ignore pode suprimir o valor de retorno de uma função, caso se deseja retornar uma unit, como o mostrado abaixo. > let square x = x * x;; val square : int -> int > ignore (square 4);;
10 val it : unit = () Tupla Uma tupla é uma coleção ordenada de dados. Ela é uma forma fácil de agrupar diferentes dados. Uma tupla é representada por um conjunto de valores separados por vírgulas e, opcionalmente, cercados por parênteses. As tuplas podem conter qualquer número de valores e de qualquer tipo. Ela pode conter até conter outras tuplas. Abaixo, são apresentados alguns exemplos de tuplas. > let zeros = (0, 0L, 0I, 0.0);; val zeros : int * int64 * bigint * float = (0, 0L, 0I, 0.0) > let nested = (1, (2.0, 3M), (4L, "5", '6'));; val nested : int * (float * decimal) * (int64 * string * char) =... Para extrair os elementos de uma tupla de dois elementos, é possível utilizar a função fst, para obter o primeiro elemento da tupla, e a função snd, para retornar o segundo elemento. A seguir são apresentados os exemplos dessas duas funções. > let nametuple = ("John", "Smith");; val nametuple : string * string = ("John", "Smith") > fst nametuple;; val it : string = "John" > snd nametuple;; val it : string = "Smith" Para extrair os elementos de uma tupla qualquer, é possível utilizar a função let seguida de múltiplos identificadores separados por vírgulas, cujos nomes armazenam os valores das tuplas. > let snacks = ("Soda", "Cookies", "Candy");; val snacks : string * string * string = ("Soda", "Cookies", "Candy") > let x, y, z = snacks;; val z : string = "Soda" val y : string = "Cookies" val x : string = "Candy" > y, z;; val it : string * string = ("Cookies", "Candy") Entretanto, ocorrerá um erro de compilação se você tentar extrair poucos ou muitos elementos de uma tupla, conforme apresentado no exemplo abaixo.
11 > let x, y = snacks;; let x, y = snacks;; ^^^^^^ stdin(8,12): error FS0001: Type mismatch. Expecting a string * string but given a string * string * string. The tuples have differing lengths of 2 and 3. Listas Em F# uma lista é definida por um conjunto de valores separados por vírgulas, e cercados por colchetes. Uma lista vazia é representada por []. A seguir são apresentados alguns exemplos de listas. let vowels = ['a'; 'e'; 'i'; 'o'; 'u'] let emptylist = [];; val vowels : char list = ['a'; 'e'; 'i'; 'o'; 'u'] val emptylist : 'a list = [] É interessante observar que nesse exemplo, a lista vazia tem um tipo 'a (genérico), pois ele pode ser qualquer tipo, sendo assim o sistema de inferência de tipo é incapaz de associar um tipo específico. Em F#, é possível incluir um elemento em uma lista, e isso é feito através do comando cons, que é representado por ::. Esse comando inclui o elemento no início da lista, conforme o apresentado abaixo. let sometimes = 'y' :: vowels;; val sometimes : char list = ['y'; 'a'; 'e'; 'i'; 'o'; 'u'] Existe um comando que permite realizar uma concatenação de duas listas de forma que se gere uma única lista contendo os elementos da primeira lista e em seguida os elementos da segunda. Esse comando é representado pelo e seu funcionamento está apresentado abaixo. let odds = [1; 3; 5; 7; 9] let evens = [2; 4; 6; 8; 10] val odds : int list = [1; 3; 5; 7; 9] val evens : int list = [2; 4; 6; 8; 10] > evens;; val it : int list = [1; 3; 5; 7; 9; 2; 4; 6; 8; 10] É possível especificar listas ordenadas numericamente sem necessariamente especificar todos os valores, o exemplo a seguir mostra exatamente isso. > let x = [1.. 10];;
12 val x : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10] Além disso, é possível gerar esse mesmo tipo de lista variando em mais de uma unidade e gerar listas ordenadas de forma decrescente. let tens = [ ] let countdown = [5L.. 1L.. 0L];; val tens : int list = [0; 10; 20; 30; 40; 50] val countdown : int list = [5L; 4L; 3L; 2L; 1L; 0L] Em F#, ainda é possível gerar listas em linha de código. Para tanto, os elementos deverão ser precedidos pelo comando yield. Além disso, cada elemento deverá estar em uma linha diferente e não deverão ser utilizadas vírgulas para separá-los. Os exemplos a seguir mostram exatamente isso. // Exemplo1 let numbersnear x = [ yield x - 1 yield x yield x + 1 ];; val numbersnear : int -> int list > numbersnear 3;; val it : int list = [2; 3; 4] // Exemplo2 let x = [ let negate x = -x for i in do if i % 2 = 0 then yield negate i else yield i ];; val x : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10] É possível simplificar o código que utiliza loops para gerar uma lista. Para tanto basta utilizar o comando -> em vez do do yield, conforme o apresentado abaixo. let multiplesof x = [ for i in do yield x * i ] let multiplesof2 x = [ for i in > x * i ] Há uma biblioteca disponível para a linguagem F# específica para a manipulação de listas. A seguir iremos apresentar seus principais comandos.
13 Função Tipo Descrição List.length 'a list -> int Retorna o tamanho da lista. List.head 'a list -> 'a Retorna o primeiro elemento da lista. List.tail 'a list -> 'a list Retorna a lista fornecida sem o seu primeiro elemento. List.exists ('a -> bool) -> 'a list -> bool Retorna um Boolean informando se a lista satisfaz a função de busca. List.rev 'a list -> 'a list Retorna a lista com a ordem dos seus elementos invertida. List.tryfind ('a -> bool) -> 'a list -> 'a option Retorna Some(x), onde x é o primeiro elemento que para uma determinada função retorna true. Caso contrário retorna None. List.zip 'a list -> 'b list -> ('a * 'b) list Dada duas listas com o mesmo número de elementos, essa função retorna uma lista de tuplas composta por essas duas listas. List.filter ('a -> bool) -> 'a list -> 'a list Retorna uma lista contendo apenas os elementos, para os quais a função fornecida retorna true. List.partition ('a -> bool) -> 'a list -> ('a list * 'a list) Dada uma lista e uma função, retorna duas listas, onde a primeira contém os elementos que retornaram true e a segunda os elementos que retornaram false. O exemplo a seguir, mostra a forma de utilização da função List.partition. let ismultipleof5 x = (x % 5 = 0) let multof5, nonmultof5 = List.partition ismultipleof5 [1.. 15];; val ismultipleof5 : int -> bool val nonmultof5 : int list = [1; 2; 3; 4; 6; 7; 8; 9; 11; 12; 13; 14] val multof5 : int list = [5; 10; 15] Operadores de agregação (aggregate operators) Os operadores de agregação são um conjunto poderoso de funções, que são úteis para qualquer coleção de valores. List.map
14 A List.map é uma operação de projeção que cria uma nova lista baseada em uma função fornecida. Cada elemento dessa nova lista é o resultado da avaliação dessa função. O exemplo a seguir mostra exatamente isso. > let squares x = x * x;; val squares : int -> int > List.map squares [1.. 10];; val it : int list = [1; 4; 9; 16; 25; 36; 49; 64; 81; 100] List.fold Fold representa o operador de agregação mais poderoso e conseqüentemente o mais complicado. Quando você tem uma lista de valores e quer transformar isso em um simples pedaço de arquivo, você usa fold. Existem dois tipos principais de fold que podem ser usados em listas. O primeiro é List.reduce, que tem o tipo: ('a -> 'a -> 'a) -> 'a list -> 'a List.reduce interage sobre cada elemento da lista, construindo um valor acumulado, que é o sumário da lista processada mais perto. Uma vez que todos os itens da lista foram processados, o valor do acumulador final é retornado. O valor inicial do acumulador é o primeiro elemento da lista. Podemos tomar como exemplo a função insertcommas, que recebe o acumulador e um valor e adiciona esse valor ao acumulador separado por uma virgula (comma). > let insertcommas (acc : string) item = acc + ", " + item;; val insertcommas : string -> string -> string > List.reduce insertcommas ["Jack"; "Jill"; "Jim"; "Joe"; "Jane"];; val it : string = "Jack, Jill, Jim, Joe, Jane" Agora, se o usuário deseja usar um acumulador customizado, pode usar o List.fold. A função fold recebe três parâmetros. Em primeiro lugar uma função que fornece um acumulador e uma lista de elementos que retorna um novo acumulador. Em segundo lugar um valor inicial para o acumulador. E por terceiro lugar o parâmetro final da lista para finalizar o fold. O valor de retorno da função é o estado final do acumulador. ('acc -> 'b -> 'acc) -> 'acc -> 'b list -> 'acc E um exemplo simples: > let addacctolistitem acc i = acc + i;; val addacctolistitem : int -> int -> int
15 > List.fold addacctolistitem 0 [1; 2; 3];; val it : int = 6 List.iter O operador final de agregação, iterage sobre cada elemento da lista e chama uma função que é passada como parâmetro. ('a -> unit) -> 'a list -> unit Como a função List.iter retorna uma unit, ela é predominantemente usada para avaliar os efeitos colaterais de um método passado. Por exemplo, usando List.iter para iteragir em cada item de uma lista e imprimir os valores no console. > // Using List.iter let printnumber x = printfn "Printing %d" x List.iter printnumber [1.. 5];; val printnumber : int -> unit Printing 1 Printing 2 Printing 3 Printing 4 Printing 5 Option Para representar um valor que pode ou não existir, a melhor forma de fazê-lo é usando o tipo option. Ele tem apenas dois valores possíveis: Some( a) e None. Imagine o problema de ter que fazer o parse de uma string para um inteiro. Se o string está formatado corretamente, a função deve retornar o valor inteiro; mas, e se a string estiver formatada impropriamente? No exemplo abaixo, se a função isinteger realize o parse corretamente, a função retorna Some(result); senão, ela irá retornar None. Isso possibilita a funções que receberão seu resultado a saber se para aquela entrada o resultada estará definido ou não. // Using option to return a value (or not) let isinteger str = let successful, result = Int32.TryParse(str) if successful then Some(result) else None;; val isinteger : string -> int option > isinteger "This is not an int";;
16 val it : int option = None > isinteger "400";; val it : int option = Some 400 Anatomia de um Programa F Sharp Módulos Quando um novo arquivo.fs é criado, F# coloca todo o código dentro de um modulo anônimo com o mesmo nome do arquivo de código, mas com a primeira letra capitalizada. Então, se você possui um valor chamada value1 e o seu arquivo chama-se file1.fs, é possível referir-se ao valor usando um caminho completo: File1.value1. module Alpha //Para se referir a este valor de fora deste módulo //devemos usar Alpha.x let x = 1 Módulos podem ser aninhados, ou seja, arquivos podem conter mais de um módulo. Para declarar um módulo aninhado, é preciso usar a palavra reservada modulo seguida do nome do modulo e um operador de igualdade (=). Os módulos aninhados devem ser identados para não serem confundidos com o modulo top-level. Module Utilities module ConversionUtils = //Utilities.ConversionUtils.intToString() let inttostring (x:int) = x.tostring() module ConvertBase = //Utilities.ConversionUtils.ConvertBase.convertToHex let converttohex x = sprintf "%x" x //Utilities.ConversionUtils.ConvertBase.convertToOct let converttooct x = sprint "%o" x module DataTypes = //Utilities.DataTypes.Point type Point = Point of float * float * float Namespaces
17 A alternativa aos módulos são os namespaces. Namespaces são unidades de organização de código tanto quanto módulos. A única diferença entre eles é que namespaces não podem conter valores, apenas declarações de tipos. Namespaces podem ser aninhados da mesma forma que os módulos podem. namespace PlayingCards //PlayingCard.Suit type Suit = Spade Club Diamond Heart //PlayingCards.PlayingCard type PlayingCard = Ace of Suit King of Suit Queen of Suit Jack of Suit ValueCard of int * int namespace PlayingCards.Poker //PlayingCards.Poker.PokerPlayer type PokerPlayer = Name: string; Money : int; Position : int Pode parecer estranho ter ambos, namespaces e módulos, na mesma linguagem. Módulos são otimizados para rapidamente prototipar e explorar uma solução. Namespaces, ao contrário, devem ser gerados em projetos de larga escala com uma solução orientada a objetos. Program Startup No F#, o programa começa a execução no topo do último arquivo de código, o qual precisa ser um módulo. Se ao escrever um programa e depois o programador adiciona um novo arquivo à solução, ao rodar nada acontecerá. Isso ocorre porque um novo arquivo é adicionado ao final da fila de arquivos do projeto. Par definir ponto de início mais formal, o programador pode usar o atributo [<EntryPoint>] para definir um método principal (main). Para que isso seja verdadeiro, é necessário que este método: Seja definido como o último método no último arquivo a ser compilado do projeto. Isso assegura que não ocorra nenhuma confusão sobre onde o programa deve começar. Receba um único parâmetro do tipo string array o qual conterá os parâmetros para o programa. Retornar um integer que será o código de saída do programa. [<EntryPoint>] let main (args : string[]) = let p = new Position() p.add(1,black) p.add(8,white) //...
18 Exemplos Práticos Devido ao fato de a maioria dos programadores estarem acostumado com alguma linguagem imperativa, forneceremos nessa seção um extenso comparativo entre estruturas básicas. Comparemos o F# ao seu irmão imperativo C# - que, a cada nova versão lançada, torna-se mais declarativo. Language Feature Classes with properties and default constructor F# C# type Vector(x : float, y : float) = member this.x = x member this.y = y let v = Vector(10., 10.) let x = v.x let y = v.y public class Vector double x; double y; public Vector(double x,double y) this.x = x; this.y = y; public double X get return this.x; public double Y get return this.y; Structs with properties [<Struct>] type Vector(x : float, y : float) = member this.x = x member this.y = y Vector v = new Vector(10, 10); double x = v.x; double y = v.y; public struct Vector double x; double y; let v = Vector(10., 10.) let x = v.x let y = v.y public Vector(double x,double y) this.x = x; this.y = y; public double X get return this.x; public double Y get return this.y; Vector v = new Vector(10, 10); double x = v.x; double y = v.y;
19 Multiple constructors Member functions type Vector(x : float, y : float) = member this.x = x member this.y = y new(v : Vector, s) = Vector(v.X * s, v.y * s) let v = Vector(10., 10.) let w = new Vector(v, 0.5) type Vector(x : float, y : float) = member this.scale(s : float) = Vector(x * s, y * s) let v = Vector(10., 10.) let v2 = v.scale(0.5) public class Vector double x; double y; public Vector(double x,double y) this.x = x; this.y = y; public Vector(Vector v,double s) : this(v.x * s, v.y * s) Vector v = new Vector(10, 10); Vector w = new Vector(v, 0.5); public class Vector double x; double y; public Vector(double x,double y) this.x = x; this.y = y; public double Scale(double s) return new Vector(this.x * s, this.y * s); Vector v = new Vector(10, 10); Vector v2 = v.scale(0.5); Operators type Vector(x, y) = member this.x = x member this.y = y static member (*) (a : Vector, b : Vector) = a.x * b.x + a.y + b.y let x = Vector(2., 2.) let y = Vector(3., 3.) let dp = x * y public class Vector double x; double y; public Vector(double x,double y) this.x = x; this.y = y; public double X get return this.x; public double Y get return this.y; public static double operator* ( Vector v1, Vector v2) return v1.x * v2.x + v1.y * v2.y; Vector x = new Vector(2, 2); Vector y = new Vector(3, 3); double dp = x * y;
20 Static members and properties Class properties that use let value computations in the constructor type Vector(x, y) = member this.x = x member this.y = y static member Dot(a : Vector, b : Vector) = a.x * b.x + a.y + b.y static member NormX = Vector(1., 0.) let x = Vector(2., 2.) let y = Vector.NormX let dp = Vector.Dot(x, y) type Vector(x, y) = let mag = sqrt(x * x + y * y) let rad = if x = 0. && y = 0. then 0. else if x >= 0. then asin(y / mag) else (-1. * asin(y / mag)) + Math.PI member this.mag = mag member this.rad = rad let v = Vector(10., 10.) let mag = v.mag let rad = v.rad public class Vector double x; double y; public Vector(double x,double y) this.x = x; this.y = y; public double X get return this.x; public double Y get return this.y; public static doubledot(vector v1, Vector v2) return v1.x * v2.x + v1.y * v2.y; public static Vector NormX get return newvector(1, 0); Vector x = new Vector(2, 2); Vector y = Vector.NormX; double dp = Vector.Dot(x, y); public class Vector double mag = 0.0; double rad = 0.0; public Vector(double x,double y) this.mag = Math.Sqrt(x * x + y * y); if (x == 0.0 && y == 0.0) rad = 0.0; else if (x >= 0.0) rad = Math.Asin(y / mag); else rad = (-1.0 *Math.Asin(y / mag)) + Math.PI; public double Mag get return this.mag; public double Rad get return this.rad; Vector v = new Vector(10, 10); double mag = v.mag; double rad = v.rad;
21 Class members that use private function values a a 6.) type Vector(x, y) = let rotate a = let x' = x * cos a y * sin let y' = y * sin a + y * cos new Vector(x', y') memberthis.rotatebydegrees(d) = rotate (d * Math.PI / 180.) memberthis.rotatebyradians(r) = rotate r let v = Vector(10., 0.) let x = v.rotatebydegrees(90.) let y = v.rotatebyradians(math.pi / public class Vector double x = 0.0; double y = 0.0; Vector rotate(double a) double xx = this.x *Math.Cos(a) this.y *Math.Sin(a); double yy = this.y *Math.Sin(a) + this.y *Math.Cos(a); return new Vector(xx, yy); public Vector(double x,double y) this.x = x; this.y = y; public VectorRotateByDegrees(double d) return rotate(d * Math.PI / 180); public VectorRotateByRadians(double r) return rotate(r); Vector v = new Vector(10, 10); Vector x = v.rotatebydegrees(90.0); Vector y = v.rotatebyradians(math.pi / 6.0); Overloading members type Car() = member this.drive() = this.drive(10) () member this.drive(mph : int) = // Do something () let c = Car() c.drive() c.drive(10) public class Car public void Drive() Drive(10); public void Drive(int mph) // Do something Car c = new Car(); c.drive(); c.drive(10); Mutable fields in a class with get/set properties type MutableVector(x : float, y : float) = let mutable cx = x let mutable cy = y member this.x with get() = cx and set(v) = cx <- v member this.y with get() = cy and set(v) = cy <- v member this.length = sqrt(x * x + y * y) let v = MutableVector(2., 2.) let len1 = v.length v.x <- 3. v.y <- 3. let len2 = v.length public class MutableVector public MutableVector(doublex, double y) this.x = x; this.y = y; public double X get; set; public double Y get; set; public double Length get return Math.Sqrt( this.x * this.x + this.y *this.y); MutableVector v = newmutablevector(2.0, 2.0); double len1 = v.length; v.x = 3.0; v.y = 3.0; double len2 = v.length;
22 Generic classes and function arguments Generic classes and methods Extension methods type Factory<'T>(f : unit ->'T) = member this.create() = f() let strings = Factory<string>( fun () -> "Hello!") let res = strings.create() let ints = Factory<int>(fun() -> 10) let res = ints.create() type Container<'T>(a : 'T) = memberthis.convert<'u>(f : 'T - >'U) = f a let c = newcontainer<int>(10) let b = c.convert(fun a - >a.tostring()) type List<'T> with memberthis.myextensionmethod() = this > Seq.map (funa - > a.tostring()) let c = [1; 2; 3] let d = c.myextensionmethod() public class Factory<T> Func<T> creator; public Factory(Func<T> f) this.creator = f; public T Create() return this.creator(); Factory<string> strings = new Factory<string>(() =>"Hello"); string res1 = strings.create(); Factory<int> ints = newfactory<int>(() => 10); int res2 = ints.create(); public class Container<T> private T value; public Container(T t) this.value = t; public U Convert<U>(Func<T, U> f) return f(this.value); Container<int> c = newcontainer<int>(10); string result = c.convert(n => n.tostring()); public static classextensionmethods public staticienumerable<string> MyExtensionMethod<T>(thisList<T> a) return a.select(s => s.tostring()); List<int> l = new List<int> 1, 2, 3 ; IEnumerable<string> res = l.myextensionmethod(); Extension properties type List<'T> with memberthis.myextensionprop = this > Seq.map (funa - > a.tostring()) let c = [1; 2; 3] let d = c.myextensionprop N/A. C# does not support this feature. Indexers type Table() = member this.item with get(key : string) = int key let tab = Table() let x = tab.["10"] let y = tab.["12"] public class Table public int this[string key] get returnconvert.toint32(key); Table tab = new Table(); int x = tab["10"]; int y = tab["12"];
23 Indexed Properties Abstract classes Derive from a base class and overriding base methods with generics Calling a base class method key key2 type Table() = member this.values with get(key : string) = int memberthis.multiplevalues with get(key1, key2) = key1 + let tab = Table() let x = tab.values("10") let y = tab.values("12") let a = tab.multiplevalues(10, 5) [<AbstractClass>] type Shape() = abstract Name : stringwith get abstract Scale : float -> Shape [<AbstractClass>] type Shape<'T>() = abstract Name : stringwith get abstract Scale : float -> 'T type Vector(angle, mag) = inherit Shape<Vector>() member this.angle = angle member this.mag = makg override this.name ="Vector" overridethis.scale(factor) = Vector(angle, mag * factor) let v = Vector(45., 10.) let v2 = v.scale(0.5) type Animal() = member this.rest() = // Rest for the animal () type Dog() = inherit Animal() member this.run() = // Run base.rest() let brian = new Dog() brian.run() N/A. C# does not support this feature. public abstract class Shape public abstract string Name get; public abstract ShapeScale(double scale); public abstract class Shape<T> public abstract string Name get; public abstract T Scale(double scale); public class Vector :Shape<Vector> double angle; double mag; public double Angle getreturn angle; public double Mag getreturnmag; public Vector(double angle,double mag) this.angle = angle; this.mag = mag; public override string Name get return "Vector"; public override VectorScale(double scale) return newvector(this.angle, this.mag * scale); Vector v = new Vector(45, 10); Vector v2 = v.scale(0.5); public class Animal public void Rest() // Rest for the animal public class Dog : Animal public void Run() // Run base.rest(); Dog brian = new Dog(); brian.run();
24 Implementing an interface Implementing an interface with object expressions type IVector = abstract Mag : doublewith get abstract Scale : float -> IVector type Vector(x, y) = interface IVector with member this.mag = sqrt(x * x + y * y) member this.scale(s) = Vector(x * s, y * s) :> IVector member this.x = x member this.y = y let v = new Vector(1., 2.) :> IVector let w = v.scale(0.5) let mag = w.mag type ICustomer = abstract Name : stringwith get abstract Age : int withget let CreateCustomer name age = new ICustomer with member this.name = name member this.age = age let c = CreateCustomer"Snoopy" 16 let d = CreateCustomer"Garfield" 5 interface IVector double Mag get; IVector Scale(double s); class Vector : IVector double x; double y; public Vector(double x,double y) this.x = x; this.y = y; public double X get return this.x; public double Y get return this.y; public double Mag get return Math.Sqrt( this.x * this.x + this.y * this.y); public IVector Scale(doubles) return new Vector(this.x * s, this.y * s); IVector v = new Vector(1, 2); IVector w = v.scale(0.5); double mag = w.mag; N/A. C# does not support creating object expressions.
25 Events type BarkArgs(msg:string) = inherit EventArgs() member this.message = msg type BarkDelegate = delegate of obj * BarkArgs - > unit type Dog() = let ev = newevent<barkdelegate, BarkArgs>() member this.bark(msg) = public class BarkArgs : EventArgs private string msg; public BarkArgs(string msg) this.msg = msg; public string Message get return this.msg; Explicit class constructor Explicit public fields Explicit struct definition ev.trigger(this, newbarkargs(msg)) [<CLIEvent>] member this.onbark = ev.publish let snoopy = new Dog() snoopy.onbark.add( fun ba - > printfn "%s"(ba.message)) snoopy.bark("hello") type Vector = val mutable x : float val mutable y : float new() = x = 0.; y = 0. let v = Vector() v.x <- 10. v.y <- 10. type Vector() = [<DefaultValue()>] val mutable x : int [<DefaultValue()>] val mutable y : int let v = Vector() v.x <- 10 v.y <- 10 [<Struct>] type Vector = val mutable public x : int val mutable public y : int let mutable v = Vector() v.x <- 10 v.y <- 10 public delegate voidbarkdelegate( Object sender, BarkArgsargs); class Dog public event BarkDelegateOnBark; public void Bark(string msg) OnBark(this, newbarkargs(msg)); Dog snoopy = new Dog(); snoopy.onbark += newbarkdelegate( (sender, msg) => Console.WriteLine(msg.Message)); snoopy.bark("hello"); public class Vector public int x; public int y; Vector v = new Vector(); v.x = 10; v.y = 10; public struct Vector public int x; public int y; Vector v = new Vector(); v.x = 10; v.y = 10;
26 Conclusões A linguagem F#, desde suas origens como projeto de pesquisa no Microsoft Reserach, Combridge, passando pela herança de linguagens como OCaml e Haskell, está agora se firmando com uma ferramenta eficiente, estável e boa de se trabalhar dentro do.net. O F# combina a simplicidade e a elegância de uma linguagem funcional tipada com a força do framework.net. Embora o paradigma funcional seja novo para a maioria dos programadores e exija um tempo de aprendizado e dedicação, ele torna a tarefa de programar mais simples em muitos sentidos. Nesse texto, pudemos verificar as construções básicas com as quais construímos soluções complexas. Verificamos que é mais fácil escrever um programa sem muitos adereços sintáticos, e que isso nos faz pensar e cuidar mais do nosso código, gerando programas melhores. Com os tipos primitivos e os core types, graças à grande ortogonalidade inerentemente funcional presente no F#, os programadores tem a disposição uma grande caixa de ferramentas seja ela para ser usada em projetos rápidos para extrair dados de muitos gigas de logs ou em grandes e complexas soluções de sistemas reais. Como uma linguagem moderna, F# possui tipos genéricos para aumentar a modularização e o reuso de código, além de também ser orientado a objetos. Na última seção acompanhamos um comparativo entre o F# e seu irmão imperativo, o C#. Aqui fica evidente que a programação funcional não é um subconjunto da imperativa, podendo ir muito além, de forma mais clara e sucinta. Nesse trabalho buscamos abordar os conceitos básicos principais para que aquele que leia saia escrevendo algumas idéias recursivamente. Ele foi, em grande parte composto por notas particulares dos autores enquanto estudam o tema. É, portanto, um documento de quem aprende para quem quer a- prender, ficando em um nível mais básico do que livros sobre o tema de autores tradicionais.
27 Bibliografia Microsoft Research. (2010). The F# 2.0 Language Specification. Microsoft. Smith, C. (2009). Programming F#. Sebastopol: O'Reilly Media, Inc. Syme, D., Granicz, A., & Cisternino, A. (2010). The Definitive Guide to F#. Ney York: Apress.