Início > Ars Physica > The Joy of Haskell, parte 1 – Introdução

The Joy of Haskell, parte 1 – Introdução

segunda-feira, 18 abr 2011; \16\UTC\UTC\k 16 Deixe um comentário Go to comments

Como alguns devem ter percebido, eu me tornei recentemente um entusiasta da linguagem de programação chamada Haskell. Eu até criei um blog para postar sobre isso, e coloquei propaganda dele aqui. O Rafael Lopes de Sá me convenceu de que não era necessário fazer isso e que eu poderia mesmo postar aqui sobre isso, então resolvi fazer um post introdutório sobre Haskell, para que os meus posts seguintes façam mais sentido.

Porque Haskell?

O que é tão atraente nessa linguagem para mim? É uma linguagem um pouco estranha, com uma curva de aprendizado um pouco complicada e as vezes parece necessário um esforço bem maior para fazer coisas que são simples em outras linguagens. Porque não C? O que Haskell faz que C não faz? Ora, nada. C e Haskell são duas formas diferentes de escrever essencialmente os mesmos comandos fundamentais. Mas há razões para eu preferir Haskell.

O primeiro é que Haskell é uma linguagem de altíssimo nível. Calma, isso não é um juízo de valor. Uma linguagem de baixo nível é uma linguagem que opera com poucas camadas de abstração entre o código e o que será de fato executado na máquina. C permite fazer operações de baixo nível: manipular memória, executar instruções asm, etc… Linguagens de alto nível escondem esses detalhes em abstrações lógicas, de modo você que nunca vai precisar se preocupar com detalhes de hardware. Haskell é uma linguagem que opera em níveis de abstrações muito mais altos que o normal. Haskell existe em um universo bastante independente do fato de que aquilo será compilado e executado em um computador físico. É uma linguagem bastante matemática e abstrata. E mesmo assim é eficiente. Há outras linguagens abstratas de alto nível – como Perl, Lisp, ML,… Mas os compiladores de Haskell são particularmente eficientes, e capazes de produzir códigos que rodam com pouca perda de desempenho se comparados a C.

O segundo é que Haskell agrega certas características que a tornam muito próximas de um sistema lógico-matemático. Muito de Haskell é baseado em uma formulação da matemática chamada Cálculo Lambda. O cálculo lambda é uma sistema formal alternativo ao sistema baseado em teoria de conjuntos. Ele pode ser usado para re-escrever a matemática que conhecemos em outras bases. Haskell é muito próximo de algo chamado cálculo lambda tipado. Isso por si é bem atraente, e há matemáticos que usam Haskell para criar sistemas formais para provar teoremas, etc. Mas para mim o melhor disso é que criar mesmo estruturas matemáticas úteis (espaços vetorias, grupos, espaços de probabilidade,etc…) em Haskell é bem natural.

Haskell é expressivo. Isso é um valor muito subjetivo, mas eu acho que o sistema de tipos de Haskell permite expressar conceitos de forma muito legível. O tipo de um objeto em Haskell traz muita informação sobre o que aquele objeto faz. Muitas vezes basta saber o tipo para saber exatamente como aquilo deve ser implementado.

Finalmente, Haskell é um exercício intelectual interessante. Eu acho estimulante programar em Haskell e acho que me fez um programador melhor mesmo quando estou usando outras linguagens.
Há outras vantagens no Haskell – como o fato de que a trasparência referencial e a avaliação não-estrita tornarem a paralelização de programas algo bem simples. Alguns dizem que linguagens influenciadas por Haskell (como as últimas versões de C#, Scala, OCaML, …) são linguagens do futuro, pois permitem fazer computação concorrente e paralela de forma trivial. Eu acho isso um overstatement, mas certamente elas vão ficar importantes.

Mas eu realmente não ligo muito pra isso. Eu gosto de Haskell como um fim em si. É quase como se fosse o meu Sudoku ou o meu Cubo de Rubik – um passatempo intelectual interessante. Isso tudo não faz de Haskell uma linguagem melhor por si só, mas quando combinado com um certo tipo de personalidade de programador, a combinação fica bem eficiente.

O que é Haskell?

Haskell é uma linguagem de programação com algumas características:

  • é funcional,
  • pura (com transparência referencial),
  • não-estrita,
  • com tipos estáticos,
  • polimorfica (com variáveis de tipo e typeclasses)

todas essas coisas serão entendidas ao longo de vários posts. São conceitos abstratos de computação que são mais fáceis de entender quando vemos um código.

Em primeiro lugar, antes de falar sobre isso, vamos ver como usar o Haskell. Você pode facilmente preparar seu computador para programar em Haskell instalando algo chamado Haskell Platform – no Ubuntu pode ser instalado via apt-get ou synaptic, no windows e em outros sabores de linux é só baixar do site o binário (http://hackage.haskell.org/platform/). O haskell platform vem com várias ferramentas, entre elas o compilador Glasgow Haskell Compiler (GHC), que contém também um modo interativo chamado GHCi, e o gerenciador de pacotes Cabal, que é usado para instalar bibliotecas de maneira bem simples.

O GHCi é um modo interativo, bem parecido com o que se encontra em outras linguagens como Python. É um loop com um cursor que recebe comandos. Você pode colar um pedaço de código lá e ele será executado. É a melhor forma de começar a aprender. Você pode começar digitando uma expressão matemática qualquer e receber a resposta assim que pressionar `Enter`:

/> 2 + 2
 4
/> 4 * 5 + 2
 22

Um programa em Haskell é essencialmente isso: uma expressão cujo valor deve ser encontrado através de um processo de “avaliação” (evaluation – não sei se essa é a melhor tradução). Essa é a essência de uma linguagem funcional – o fato de que o código é escrito como uma expressão, resultado da aplicação de funções sobre outras expressões, e o objetivo final do código é encontrar o valor daquela expressão. Algo como aqueles exercícios que você fazia na 6ª série: “Efetue: …” e uma expressão complicada com números e operadores era dada, para que você encontrasse o valor daquela expressão.

Todo objeto em Haskell é uma expressão, com um valor bem definido. E todo valor tem um tipo bem definido. Há tipos básicos, Int (inteiros), Char (caracteres), Double (ponto flutuante de dupla precisão), Bool (booleanos), etc… e tipos mais abstratos, sobre os quais vou falar mais tarde – os tipos algébricos, que são o “bread and butter” do Haskell. Você pode dar um nome para uma expressão, para resumir a construção de outras expressões. Se você editar um arquivo chamado “teste.hs” com o seguinte conteúdo:

> -- arquivo teste.hs
> x :: Int
> x = 20
> y :: Int
> y = 5

Você pode carregá-lo no GHCi da seguinte forma:

/> :l teste.hs
[1 of 1] Compiling Main ( foo.hs, interpreted )
Ok, modules loaded: Main.
/> x
20
/> x + y
25
/> let z = x + y
/> z
2

Para criar nomes dentro do GHCi é preciso usar a cláusula “let”. Em um arquivo separado não é preciso.

Note a sintaxe: x::Int. Isso significa “vou dar o nome x a uma expressão que tem tipo Int”. Isso não é uma declaração de variáveis como é comum em linguagens imperativas. Isso é só a definição de um nome. Não existem “variáveis” em Haskell como existe em C, um espaço reservado de memória cujo conteúdo pode ser manipulado. Um nome é um nome definitivo – aquele nome é um mero sinônimo para a expressão que ele representa. Um nome não pode mudar de valor. Se você tentar colocar:

> x :: Int 
> x = 10
> x = 20

em um arquivo, quando tentar carregá-lo obterá um erro:

Multiple declarations of `Main.x'
 Declared at: dah.hs:1:0
 dah.hs:3:0

Alguns tipos algébricos importantes são fornecidos na biblioteca padrão, chamada Prelude. Por exemplo, as listas:

> xs :: [Int]
> xs = [3,4,32]

Uma lista é uma sequencia ordenada de valores do mesmo tipo. Uma lista de objetos de tipo a tem tipo [a]. String é um sinônimo para [Char]:

/> :t "rafael"
"rafael" :: [Char]
/> :t ("rafael" :: String)
 ("rafael" :: String) :: String

Outro tipo composto comum é na verdade uma família de tipos chamados “tuplas”. São sequencias ordenadas de objetos de tipos que podem ser iguais ou não:

> tupla :: (Int, Double)
> tupla = (5, 3.14)

A principal diferença entre tuplas e listas é que listas de tamanhos diferentes tem o mesmo tipo, desde que os objetos internos seja do mesmo tipo:

> ys :: [Int]
> ys = [2,3,2,3,4,22,3,2,4,2,4]

Já tuplas de tamanhos diferentes têm tipos diferentes:

> tupla' :: (Int, Int, Double)
> tupla' = (5, 3, pi)

Tuplas e listas podem ser combinadas para fazer tipos bem complicados:

> foo :: [(Int, Double)]
> foo = [(1, 1.2), (3, 2.3)]
> bar :: (Int, (Int, Char), [String])
> bar = (1920, (0, 'a'), ["Haskell", "É", "Legal"])

Se eu não posso alterar que expressão um nome armazena, como faço computação em Haskell? Eu crio funções. Funções são objetos que recebem uma entrada e retorna uma saida. Por isso elas têm tipo (a -> b) onde a é o tipo do valor de entrada e b o tipo do valor de saída:

> succ, pred :: Int -> Int
> succ x = x + 1
> pred x = x - 1

Uma computação em Haskell é feita através da aplicação de funções sobre expressões – múltiplas funções são combinadas para que, quando aplicadas a uma expressão de entrada, produzirem o resultado desejado. Em Haskell funções são expressões como outras quaisquer. Elas podem ser entradas e saídas de outras funções. Eu posso ter uma função que recebe outra função como parâmetro ou retorna outra função como resultado. Por exemplo, eu posso ter uma função:

> foo' :: (Int -> Int) -> Int
> foo' f = (f 0)

Para qualquer função f::(Int -> Int), essa função retorna o valor de f calculado em 0. No futuro veremos casos mais úteis disso. Eu disse que funções são expressões como outras quaisquer. Se eu posso escrever expressões sem ter que dar um nome para elas, tem que haver um jeito de fazer isso com funções também, né? São as chamadas expressões lambda (também conhecidas como “closures” em outras linguagens). Eu posso aplicar a função foo’ em uma função anonima assim:

/> foo' (\ x -> x + 1)
1

A expressão (\ x -> expr ) deve ser interpretada assim: uma função que, quando recebe um número x, retorna expr. Portanto (\ x -> x + 1) é a função que quando aplicada em x, retorna x+1. Se eu avaliar a expressão foo’ (\ x -> x + 1) terei:

foo' (\ x -> x + 1)
(\x -> x + 1) 0
0 + 1
1

As vezes funções que recebem ou retornam funções são chamadas de funções de segunda ordem.

Eu disse que funções tem apenas uma entrada e uma saída. Mas e quando eu preciso de mais de um dado de entrada para produzir um resultado? Eu posso usar tuplas:

> soma :: (Int, Int) -> Int
> soma (x,y) = x + y

Não parece muito prático. Mas existe um teorema em cálculo Lambda que diz o seguinte: uma função de duas entradas é isomórfica a uma função que recebe apenas uma entrada e retorna uma função. Como assim? . Imagine que eu reescrevesse a função soma assim:

> soma :: Int -> (Int -> Int)
> soma x = (\y -> x + y)

O que acontece quando eu aplico essa função em um número? O resultado de (soma 1) é (\y -> 1 + y). Ou seja, (soma 1) é uma função que quando aplicada em y, me retorna 1 + y. Então ((soma 1) 2) = 3. Para facilitar a vida do programador, o compilador de haskell entende que (soma 1 2) significa: aplique soma ao número 1 e depois aplique a função resultante ao número 2. O compilador também permite escrever:

> soma :: Int -> Int -> Int
> soma x y = x + y

O compilador também entende a sintaxe (\ x y -> expr…). Isso é traduzido automaticamente para (\ x -> (\ y -> expr…)). A aplicação da função sobre um valor retornando uma função que aguarda outro valor é por vezes denominada aplicação parcial.

Note que o tipo a -> b -> c deve ser sempre interpretado como a -> (b -> c), nunca como (a -> b) -> c. Note a diferença: o primeiro recebe um valor e retorna uma função, o segundo recebe uma função e retorna um valor.

Você já percebeu nesse momento uma coisa: todos os nomes de expressões são em minúsculas e os nomes de tipos em maiúsculas. E também já percebeu que de vez em quando eu uso a, b, c para denotar um tipo qualquer. O compilador de Haskell entende isso. Isso é chamado polimorfismo paramétrico. Para entender vamos dar uma olhada na função de segunda ordem que implementa o padrão mais tipico do Haskell: a composição de funções. Suponha que eu queira aplicar as funções succ e pred em sequencia sobre um número. Eu posso fazer:

/> pred (succ 0 )
 0

Note que eu posso aplicar a combinação (pred (succ x)) em qualquer x. Eu posso ter a função

> bar' = (\ x -> pred (succ x))

Esse é um padrão independente do que fazem as funções pred e succ. Aliás, é independente do fato de que eu estou falando de números inteiros. Apenas depende de um fato: eu tenho duas funções cujos tipos são compatíveis: o tipo que succ retorna é compatível com o tipo de entrada de pred. Considere a seguinte função de segunda ordem:

> composicao f g x = f (g x)

Ela recebe duas funções (f e g) e um valor (x) e retorna a aplicação sucessiva dessas funções ao valor dado. Qual é o tipo dessa função? Vamos deixar o compilador inferir para nós:

/> :t composicao
composicao :: (t1 -> t2) -> (t -> t1) -> t -> t2

Ou seja: quaisquer que sejam as funções f e g, essa função faz sentido, desde que o tipo de saida de g seja o tipo de entrada de f. Essa função já existe na biblioteca padrão, e é chamada (.). Sua definição é:

> (.) :: (b -> c) -> (a -> b) -> a -> c
> (f . g) x = f (g x)

A aplicaçao parcial de (.) sobre duas funções retorna a função composta (no sentido matemático mesmo). Ou seja: (f.g) = (\ x -> f (g x)). Isso funciona para quaisquer tipos a,b e c e quaisquer funções que satisfaçam os tipos da declaração de (.). Isso quer dizer que (.) é uma função polimórfica – ou seja, uma família de funções que pode ser especializada para vários tipos diferentes. Exemplos:

> foo = sin . cos
> bar = succ . length

A função foo é a composição das funcções sin :: Double -> Double e cos :: Double -> Double. O resultado de (foo x) é sin(cos x). A função bar é a composição de succ, que calcula o sucessor de um número inteiro, com a função length, que calcula o tamanho de uma lista de um tipo qualquer:

> length :: [a] -> Int
> succ :: Int -> Int
> (succ . length) :: [a] -> Int

Funções desse tipo – que compõe operações simples para realizar operações complexas, são o arroz com feijão da programação funcional. Quase todas as funções importantes da biblioteca básica são funções polimórficas de segunda ordem. Veremos várias em breve – map, ($), (>>=), replicate, concat, concatMap, zip, …

Enfim. Acho que é o suficiente por hoje. Na próxima vou falar das coisas mais legais do Haskell que são os tipos algébricos e as classes. Depois vou discutir um pouco sobre as características da linguagem – o fato de ser fortemente tipada, de ter transparência referencial e avaliação não-estrita.

Categorias:Ars Physica
  1. Nenhum comentário ainda.
  1. segunda-feira, 22 ago 2011; \34\UTC\UTC\k 34 às 11:42:49 EST

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: