20 de janeiro de 2013

C, C++, Haskell, Java?

Estou empolgado e agoniado com meu aprendizado sobre OpenGL. Ao mesmo tempo em que aprender OpenGL me deixa muito satisfeito, também me deixa inquieto: dediquei muito tempo aprendendo Haskell e fui envolvido de uma forma a gostar muito da linguagem e da comunidade. Porém, fazer algo em OpenGL se mostrou muito difícil e até mesmo solitário. Existem pouquíssimos materiais sobre isso e a maioria deles estão obsoletos.

Tive que largar Haskell por um tempo e religar os neurônios da linguagem C para aprender o fluxo das versões mais modernas do OpenGL, com shaders e buffer objects. O motivo foi simples: não consegui fazer nada funcionar em Haskell e não tive certeza se a culpa era minha ou das bibliotecas que utilizava.

Pois bem, fazer uma câmera se movimentar em um mundo 3D em C também não foi nada fácil. Mas com a abundância de material, consegui deixar funcionando de uma forma que me deixou bem satisfeito.

Decidi então voltar para Haskell e aplicar este conhecimento que acabei de conquistar.

Que trabalheira! Fazer tudo se encaixar da maneira certa e com os tipos corretos me fez suar algumas camisas. Se não fosse tão teimoso, não teria conseguido. Tive que trocar o pacote OpenGL pelo OpenGLRaw pois o pacote OpenGL não tem binding para "glUniformMatrix*"; também troquei o GLFW pelo GLFW-b porque o primeiro dependia do pacote OpenGL.

Recentemente li um artigo no InfoQ questionando se a linguagem C ainda era útil. Me fez pensar em muita coisa, inclusive em abandonar definitivamente Haskell e voltar às origens: ponteiros, gerenciamento de memória manual, strings cabulosas, ...

Talvez seja porque sou muito novo neste mundo, mas me sinto meio improdutivo programando em Haskell. A impressão que tenho é que, a menos que seja algo muito bem consolidado e utilizado por muitas pessoas da comunidade, muito tempo será gasto atrás de bibliotecas que normalmente são apenas um binding para uma escrita em C. Quando não for isto, só para ter uma breve ideia do que a API de uma biblioteca faz, acabo lendo alguns papers relembrando o que são Functors e Monads (para entender Arrows acho que vou precisar comprar um livro!).

Entender ponteiros e gerenciamento de memória em C também foi difícil. Mas naquela época eu era um jovenzinho no mundo da programação, meu conhecimento era muito limitado, se baseava muito em bater no código até que ele fizesse o que eu queria. Agora não consigo mais aceitar fazer alguma coisa sem entender o que estou fazendo.

Eu lembro que tive bons motivos para trocar C por Java. Depois de passar um tempo utilizando Java, vejo também as partes negativas da linguagem. Meu trabalho de conclusão da graduação foi dedicado a evidenciar os motivos desta troca e do futuro promissor de linguagens como Haskell.

No final das contas, acredito que esta pseudo-decepção acontece com toda linguagem em que você dedica bastante tempo para aprendê-la. A propaganda e os primeiros passos são maravilhosos. Parece que você descobriu um novo mundo onde todos são ricos e podem desfrutar de todas as coisas boas. Com o tempo você percebe que não é bem assim: cada escolha que a linguagem fez significa pontos positivos e negativos para nós usuários. Foi assim com C, Python, Java e agora com Haskell.

Java escolheu uma abordagem orientada a objetos: mesmo que seja uma função estática ou uma variável global, será necessária a roupagem de uma classe para acomodar este código. Haskell escolheu o funcional puro: mesmo operações que envolvam I/O recebem a roupagem de um monad.

Não que isto seja ruim. A utilização de monads para modelar computação sequencial foi uma grande descoberta que proporcionou a criação de inúmeras soluções. Estas escolhas trazem pontos positivos e negativos. Só devemos ter cuidado com o purismo extremo para não criar aberrações (e.g. Singleton).

Enfim, estou agoniado pois não sei o que escolher:

  • C me parece flexível mas com as complexidades que não quero ter. C11 parece solucionar algumas coisas (e.g. <threads.h>), mas o GCC ainda não suporta;
  • Sou bem produtivo em Java mas o overhead obrigatório e a necessidade de ter uma JVM me irritam profundamente;
  • C++ ainda é uma incógnita para mim, se for para ir neste nível ainda prefiro utilizar C. Java já me fez ver os motivos para dizer não à O.O. Por outro lado, boa parte de quem utilizava C está indo para C++, inclusive o GCC está utilizando C++. As bibliotecas disponíveis para C++ também pesam bastante (e.g. GLM, Boost);
  • Haskell é minha linguagem da vez mas está começando a me incomodar um pouco, sentimento de improdutividade me deixa muito tenso. Talvez precise dedicar mais tempo para que isto passe. Não sei se este esforço irá valer a pena ou poderia ser melhor aproveitado aprimorando meus conhecimentos em outra linguagem.

A diferença de desempenho entre C e Haskell não me preocupa muito, pois não vou escovar bits para ter um bom desempenho tão cedo.

No final das contas, a opinião mais correta sempre é: depende do projeto, escolha a melhor linguagem para o seu projeto. Pois bem, não tenho projeto nenhum, mas quero me aprofundar no desenvolvimento de jogos.

E agora?

9 de janeiro de 2013

DuDuHoX com som: OpenAL

Trabalhar no DuDuHoX está cada vez mais difícil. Já pensei em desistir em alguns problemas que tive, seja com código ou com bibliotecas.

Colocar animação no jogo foi muito complicado. Porque o motor do jogo não tem estados intermediários em que o personagem está indo de uma casa para outra. O estado do jogo só permite que o personagem ocupe uma casa e não metade de uma e metade de outra.

Minha solução para isto foi colocar a informação de animação no código da interface gráfica (OpenGL). Após pressionar alguma tecla que movimente o personagem, o motor do jogo já estará no estado novo completo, mas a interface não aceitará mais nenhuma entrada enquanto não terminar de exibir a animação de movimentação.

Outro grande problema que tive foi quando decidi colocar sons no jogo. Meus objetivos são: música de fundo e sons posicionais. A biblioteca que achei para isto foi a OpenAL. Ela é feita em C mas existe o pacote Haskell que faz o binding.

Mas, nem tudo é tão bonito. Instalar a biblioteca OpenAL no Windows foi um tremendo caos. Foi uma tarde inteira tentando fazer isto. Quando finalmente consegui instalar, percebi que também iria precisar da biblioteca ALUT. Esta não foi tão difícil de instalar, porém, ocorriam erros de link durante a compilação. Felizmente encontrei um bug reportado para o GHC que tratava exatamente do meu problema: Ticket 1243. Depois de aplicar o patch, tudo funcionou.

Fiz uma música no OpenMPT, desenhei o personagem no GIMP e coloquei tudo dentro do jogo.

Apesar das dificuldades, estou muito feliz com resultado que estou obtendo em fazer o DuDuHoX em Haskell. Decidi gravar um gameplay com o CamStudio e compartilhar com vocês através do YouTube:

31 de dezembro de 2012

OpenGL em Haskell

Continuo trabalhando no DuDuHoX.

A primeira interface do jogo é em console. Utiliza o pacote System.Console.ANSI. Funciona muito bem em Linux e Mac OS, apenas no Windows tem um pequeno problema de buffer, forçando com que o jogador tenha que apertar enter após cada jogada.

DuDuHoX versão console

Esta semana decidi fazer uma nova interface para o jogo: com gráficos e sem o problema de buffer do console do Windows.

Primeiramente tentei utilizar SDL, pois foi ela que utilizei para fazer DuDuRoX em C. Infelizmente, não tive sucesso nenhum, nem mesmo consegui fazer um exemplo compilar.

Pensei em utilizar diretamente OpenGL para o gráfico, mas não sabia o que utilizar para gerenciar a entrada do usuário (apertar de teclas). Já vi reclamações a respeito de OpenGL/GLUT. Foi então que depois de algumas pesquisas conheci o GLFW, um biblioteca C open-source que fazia tudo que eu precisava, e o mais importante: existe o pacote GLFW para Haskell que faz o binding.

Depois de algumas horas aprendendo OpenGL e GLFW para Haskell, o resultado é este:

DuDuHoX versão gráfica

Já está jogável com esta interface, porém a versão console possui mais funcionalidades. Então, para as próximas semanas, vou tentar passar todas as funcionalidades da versão console para a versão gráfica e adicionar algumas texturas bacanas. A lista de coisas a fazer e todo o código está disponível no GitHub: https://github.com/thiago-negri/DuDuHoX.

Feliz 2013!

15 de agosto de 2012

Programação funcional pura e útil

Acho que todos os programadores acostumados com o modelo imperativo ficam espantados em imaginar quão maluco é o modelo funcional puro. Afinal, como podemos fazer qualquer coisa útil sem gerar efeitos colaterais? Sem imprimir no console? Sem abrir uma tela? Sem gerar logs? Sem alterar variáveis? OMG.

Este também foi um espanto meu ao conhecer Haskell e por uma feliz insistência descobri que o problema está na cabeça do programador e não no modelo funcional puro.

Diferenças

Viver muito tempo no contexto das linguagens imperativas acabou modelando minha forma de pensar em uma maneira sequêncial. Eu sempre precisei dizer como fazer cada passo da computação, quais estados manter, quais variáveis alterar, quando retornar um valor, etc.

Enquanto que no mundo funcional puro, fui obrigado a pensar em uma maneira de transformação de dados. Ao invés de pensar em como fazer cada etapa, preciso pensar como transformar um dado em outro, i.e. como extrair a representação que quero a partir do dado que tenho.

Os exemplos clássicos para demonstrar esta diferença envolve a manipulação de listas.

Exemplo #1

Para calcular a somatória no estilo imperativo, é necessário pensar no controle sobre o estado da soma atual, que deve ser acumulado durante a iteração da lista.

int sum(int[] list) {
  int result = 0;
  for (int i : list)
    result += i;
  return result;
}

Do outro lado, no mundo funcional, é necessário pensar em como transformar uma lista em um único número que represente a soma de todos os elementos, i.e. a soma de uma lista vazia sempre será zero e a soma de qualquer outra lista é definida pelo seu primeiro valor acrescentado da soma do restante da lista.

sum [] = 0
sum (x:xs) = x + sum xs

Exemplo #2

Já em um exemplo um pouco mais complexo, em que precisamos manter duas listas no mundo imperativo, uma de entrada e outra de saída, precisamos incluir o controle sobre o índice de cada lista, para evitar erros de indexação.

String[] toText(int[] list) {
    String[] result = new String[list.length];
    for (int i = 0; i < list.length; ++i)
        result[i] = Integer.toString(list[i]);
    return result;
}

Enquanto que o mesmo exemplo no mundo funcional continua simples como a somatória da lista, a única diferença é que ao invés de transformarmos a lista utilizando a adição, utilizamos o próprio construtor de listas (representado pelo operador ':').

toText [] = []
toText (x:xs) = show x : toText xs

Motivação

Para demonstrar que o modelo funcional puro pode fazer muitas coisas além dos exemplos clássicos, de uma forma que considero bonita e com vantagens gratuitas, escrevo este post.

Calculadora

Hoje vamos criar uma calculadora em Haskell. Todo o código será puro, sem nenhum tipo de efeito colateral, incluindo uma DSL para escrever testes, o próprio executor dos testes e dois transformadores de cenários de teste, um para escrever o teste no formato de texto e outro para escrever o mesmo teste no formato de um JUnit.

Formato dos testes

Primeiramente, definimos o formato em que os testes serão escritos. Neste caso, será uma sequência de ações e validações:

data TestSequence =
    Do Action TestSequence
  | Check Assertion TestSequence
  | Done

Com esta estrutura de dados, os testes poderão ser escritos neste formato:

test =
  Do a.
  Do b.
  Do c.
  Check d$
  Done

A necessidade de ter o "Done" no final do teste me incomoda. Para evitar que precise escrever isto, indicarei que os testes sempre terminem de forma aberta, i.e. permitindo continuar a sequência:

type Test = TestSequence -> TestSequence

Agora podemos escrever o teste da seguinte forma:

test =
  Do a.
  Do b.
  Do c.
  Check d

Para mim, já está classificada como uma DSL funcional para testes.

O único tipo de ação disponível na calculadora é o de apertar seus botões e a única validação será a verificação do seu display. Os botões disponíveis são os padrões de uma calculadora.

data Action =
    Press Button

data Assertion =
    DisplayHasNumber Int

data Button =
    Zero
  | One
  | Two
  | Three
  | Four
  | Five
  | Six
  | Seven
  | Eight
  | Nine
  | Plus
  | Minus
  | Times
  | Divide
  | Equals
  | Clear
    deriving (Show)

Primeiro teste

O primeiro passo está pronto. Já temos uma DSL para testes compilando. Hora de escrever nosso primeiro teste, que também já compila:

sample :: Test
sample =
    Do (Press One).
    Do (Press Plus).
    Do (Press One).
    Do (Press Equals).
    Check (DisplayHasNumber 2).

    Do (Press Clear).
    Check (DisplayHasNumber 0).

    Do (Press Two).
    Do (Press Zero).
    Check (DisplayHasNumber 20).
    Do (Press Divide).
    Do (Press Two).
    Check (DisplayHasNumber 2).
    Do (Press Equals).
    Check (DisplayHasNumber 10)

Transformadores do teste

Como nosso teste está escrito na forma de uma estrutura de dados, isto nos dá muitas possibilidades para tratamento do caso de teste. Para facilitar as transformações desta estrutura, definimos uma função que aplica uma função genérica a cada passo do teste e agrupa os resultados em uma lista:

unroll :: (TestSequence -> a) -> Test -> [a]
unroll f t = g (t Done)
  where g Done = [f Done]
        g v@(Do _ next) = f v : g next
        g v@(Check _ next) = f v : g next

Teste -> Texto

Podemos processar o teste e gerar uma saída textual do cenário:

prettyPrint :: Test -> String
prettyPrint = unlines . unroll prettyPrintTestSequence

prettyPrintTestSequence :: TestSequence -> String
prettyPrintTestSequence s =
    case s of
      Done              -> "end"
      Do action _       -> prettyPrintAction action
      Check assertion _ -> prettyPrintAssertion assertion

prettyPrintAction :: Action -> String
prettyPrintAction (Press button) = 
    "press " ++ prettyPrintButton button

prettyPrintButton :: Button -> String
prettyPrintButton = map toLower . show

prettyPrintAssertion :: Assertion -> String
prettyPrintAssertion (DisplayHasNumber number) = 
    "the display should be showing the number " ++ 
    show number

Testando no GHCi:

\> putStr $ prettyPrint sample
press one
press plus
press one
press equals
the display should be showing the number 2
press clear
the display should be showing the number 0
press two
press zero
the display should be showing the number 20
press divide
press two
the display should be showing the number 2
press equals
the display should be showing the number 10
end

Teste -> JUnit

Também podemos gerar casos de teste para uma possível solução Java da nossa calculadora:

generateJUnit :: Test -> String
generateJUnit = 
    ("@Test\npublic void test() {\n" ++) . 
    unlines .
    unroll generateJUnitTestSequence

generateJUnitTestSequence :: TestSequence -> String
generateJUnitTestSequence s =
    case s of
      Done      -> "}"
      Do a _    -> generateJUnitAction a
      Check a _ -> generateJUnitAssertion a

generateJUnitAction :: Action -> String
generateJUnitAction (Press b) =
    generateJUnitButton b ++ ".press();"

generateJUnitButton :: Button -> String
generateJUnitButton b = "getButton" ++ show b ++ "()"

generateJUnitAssertion :: Assertion -> String
generateJUnitAssertion (DisplayHasNumber n) =
    "assertEquals(" ++ show n ++ ", getDisplayNumber());"

Testando no GHCi:

\> putStr $ generateJUnit sample
@Test
public void test() {
getButtonOne().press();
getButtonPlus().press();
getButtonOne().press();
getButtonEquals().press();
assertEquals(2, getDisplayNumber());
getButtonClear().press();
assertEquals(0, getDisplayNumber());
getButtonTwo().press();
getButtonZero().press();
assertEquals(20, getDisplayNumber());
getButtonDivide().press();
getButtonTwo().press();
assertEquals(2, getDisplayNumber());
getButtonEquals().press();
assertEquals(10, getDisplayNumber());
}

Teste de fato

E claro, também podemos utilizar nosso teste no formato de uma estrutura de dados para realmente testar uma calculadora:

data TestResult =
    Ok
  | Failed FailureMessage

type FailureMessage = String

instance Show TestResult where
    show Ok = "Test passed"
    show (Failed m) = "Test failed: " ++ m

checkTest :: Test -> TestResult
checkTest t = 
    evalState 
    (threadCheckState . unroll checkTestSequence $ t)
    mkCalculator

threadCheckState :: [State Calculator TestResult] ->
                    State Calculator TestResult 
threadCheckState = go 0
  where go _ [] = return Ok
        go n (x:xs) = x >>= (f n xs)
        f n xs Ok = go (n + 1) xs
        f n _ (Failed m) = 
          return . Failed $
            "Step " ++ show n ++ ". " ++ m

checkTestSequence :: TestSequence -> 
                     State Calculator TestResult
checkTestSequence Done = return Ok
checkTestSequence (Do a _) = checkAction a
checkTestSequence (Check a _) = checkAssertion a

checkAction :: Action -> State Calculator TestResult
checkAction (Press b) = do
    modify $ pressButton b 
    return Ok

checkAssertion :: Assertion -> State Calculator TestResult
checkAssertion (DisplayHasNumber n) =
    get >>= \c ->
      if displayNumber c == n
        then return Ok
        else return . Failed $ 
          "Wrong number in display, should be " ++
          show n ++ " but was " ++ show (displayNumber c)

O coração da calculadora

Finalmente, para que o testador-de-fato compile, precisamos definir uma primeira versão da nossa calculadora. Aqui você poderia tentar criar sua própria implementação. Cheguei neste código para atender ao cenário do teste:

data Calculator = Calculator {
    displayNumber :: Int
  , operation :: Maybe (Int -> Int -> Int)
  , savedNumber :: Int
  }

pressButton :: Button -> Calculator -> Calculator
pressButton b =
  case b of
    Zero    -> appendNumber 0
    One     -> appendNumber 1
    Two     -> appendNumber 2
    Three   -> appendNumber 3
    Four    -> appendNumber 4
    Five    -> appendNumber 5
    Six     -> appendNumber 6
    Seven   -> appendNumber 7
    Eight   -> appendNumber 8
    Nine    -> appendNumber 9
    Plus    -> saveOperation (+)
    Minus   -> saveOperation (-)
    Times   -> saveOperation (*)
    Divide  -> saveOperation div
    Equals  -> performOperation
    Clear   -> clear

appendNumber :: Int -> Calculator -> Calculator
appendNumber i c = 
  c { 
    displayNumber = (displayNumber c) * 10 + i 
  }

saveOperation :: (Int -> Int -> Int) -> 
                 Calculator -> Calculator
saveOperation f c = 
  c { 
    savedNumber = (displayNumber c)
  , displayNumber = 0
  , operation = Just f
  }

performOperation :: Calculator -> Calculator
performOperation c = 
  c { 
    savedNumber = newNumber
  , displayNumber = newNumber 
  }
  where newNumber = 
          case (operation c) of
            Nothing -> displayNumber c
            Just f  -> let a = savedNumber c
                           b = displayNumber c 
                        in
                           f a b

clear :: Calculator -> Calculator
clear = const mkCalculator

mkCalculator :: Calculator
mkCalculator = 
  Calculator { 
    displayNumber = 0
  , operation = Nothing
  , savedNumber = 0
  }

Testando a calculadora

Agora podemos executar o testador-de-fato contra nossa calculadora no GHCi. Vamos experimentar:

\> checkTest sample
Test passed

Isto significa que nossa calculadora está atendendo ao cenário de teste. Para garantir que o teste capture um comportamento que não corresponda ao cenário de teste, podemos introduzir um erro: alteramos o valor da multiplicação na função "appendNumber" de "10" para "100". Se rodarmos novamente o teste, teremos este resultado:

\> checkTest sample
Test failed: Step 9. Wrong number in display, should be 20 but was 200

Magavilha.

Conclusão

Construímos um código básico de uma calculadora funcionando que atende ao especificado no cenário de teste. Agora basta adicionarmos novos testes e continuar incrementando nossa calculadora, parecido com um TDD. Tudo isto foi feito de uma forma 100% pura, sem depender de nada que envolva IO, estado, variáveis ou logs.

Além do benefício de podermos fazer qualquer tipo de transformação nos cenários de teste, também podemos rodar quantas transformações e quantos testes quisermos de forma paralela, pois tanto o teste quanto a calculadora são thread-safe pelo simples fato de serem escritos de forma pura.

Conseguimos fazer tudo isto em míseras 251 linhas de código ou 5635 caracteres, incluindo comentários e linhas em branco. Quantas classes e/ou linhas seriam necessárias para contemplar todas as funcionalidades e seguranças demonstradas neste post na sua linguagem imperativa favorita?

Espero que este post sirva para inspirar vossas mentes a pensar "fora-da-caixa" do mundo imperativo. Existem muitas vantagens em aprender uma linguagem funcional pura, mesmo que você utilize linguagens imperativas para trabalhar.

O código inteiro está disponível no GitHub: https://gist.github.com/3354394. Para testar, é só baixar o código e carregar no GHCi.

Exemplos reais

Apenas para finalizar, alguns exemplos de aplicativos reais em uso desenvolvidos em Haskell:

12 de agosto de 2012

Criando um jogo de labirinto em Haskell

Quando eu estava aprendendo a programar em C, eu fiz vários pequenos projetos para colocar meu conhecimento à prova.

É muito bom olhar estes códigos antigos e perceber que aprendi muito nestes anos. Também é prazeroso ver que mesmo sem conhecer quase nada, consegui fazer muitas coisas divertidas.

Uma das primeiras coisas que fiz em C, foi um jogo de labirinto para terminal do Windows. Fiquei inacreditavelmente feliz e me achando muito inteligente por conseguir fazer um jogo assim.

Após algum tempo já programando, depois de conhecer a biblioteca SDL para criar jogos gráficos em C, o joguinho de labirinto chegou na sua terceira versão: DuDuRoX.

Como agora estou aprendendo Haskell e quero manter a tradição, irei criar um novo DuDuRoX nesta linguagem: DuDuHoX. Notem a pequena variação no nome demonstrando minha imensa criatividade.

Quem quiser acompanhar o progresso e fazer comentários, todo o código está disponível no GitHub (link no parágrafo acima).

Bastantes coisas já estão funcionando, até já é possível jogar. Quem quiser experimentar, é só baixar a plataforma Haskell, fazer um clone do repositório, executar o comando cabal install na pasta do jogo e ele estará disponível no seu terminal como "DuDuHoX".

Quem quiser dar uma olhada em códigos antigos, meus primeiros passos na programação estão postados no site Viva O Linux:

Alguns códigos mais "avançados", acabei não compartilhando, porém tenho eles salvos no meu computador, se alguém se interessar posso compartilhar também:

  • MultiMines: Jogo de campo minado.
  • HChess: Jogo de xadrez.
  • ObjectDetector: Tratador de imagem para realçar as bordas de objetos.
  • LConv: Conversor de "linguagens", a partir de uma pseudo-linguagem, o usuário conseguia definir regras de criptografia e aplicar a textos.
  • GATesting: Algoritmo genético para encontrar qualquer formula matemática que trouxesse como resultado o valor informado.

14 de julho de 2012

O futuro da programação

Postado originalmente em Seniortec.

De acordo com o índice TIOBE, em 1996 a linguagem de programação mais utilizada era C. Apenas dez anos mais tarde, em 2006, Java passou a ocupar o primeiro lugar. Por quê?

Com o baixo custo e o fácil acesso a grandes quantidades de memória, a transição de C para Java foi um reflexo natural dos programadores em busca de maneiras mais fáceis de gerenciar este recurso que crescia de maneira muito rápida.

Novamente estamos passando por uma quebra no paradigma computacional. A busca incansável por maior quantidade de ciclos é deixada de lado e da lugar à busca por paralelismo de operações e quantidade de núcleos de processamento.

As linguagens atuais estão preparadas para estas mudanças? Java ainda é bom o suficiente para manter sua hegemonia? Perguntas como estas começam a aparecer em artigos, fóruns, apresentações e congressos de nossa área.

Juntamente com estas perguntas, começa a surgir um interesse súbito por linguagens funcionais, como mostra a mensagem deste mês do índice TIOBE:

"Last month we asked ourselves the question what language could become the next big new programming language. We suggested several candidates such as Scala, Erlang and Clojure. Clearly, the new thing was expected to come from the functional programming field. A functional language not explicitly mentioned was Haskell. And this month it was Haskell that jumped from #35 to #25. Looking at the TIOBE trend graph of Haskell (starting in 2003) it shows a constant rise, with peaks in 2006, 2010 and now in 2012. This certainly sounds promising."

Todo este interesse não é preciosismo de alguns estudiosos. O paradigma funcional puro soluciona de uma forma muito suave a maioria dos problemas encontrados quando tratamos de assuntos de importância fundamental para esta nova arquitetura computacional: concorrência, paralelismo e escalabilidade.

A linguagem Haskell, por exemplo, oferece:

  • Estruturas de dados persistentes: seguras para ambientes concorrentes.
  • Operações que envolvem I/O são discriminadas de forma estática pelo sistema de tipos, o que gera uma cultura que preza pela pureza das bibliotecas. Garantindo escalabilidade de forma natural.
  • Operações que envolvem I/O são modeladas dentro de um conceito puro (Monad), permitindo composição, backtracking, computação não determinística, etc.
  • Polimorfismo aberto permite uma consolidação de uma única biblioteca e conceito, mesmo que construído de forma incompleta no primeiro release.
  • Avaliação preguiçosa possibilita um melhor gerenciamento do processamento e localidade em ambientes com escalabilidade.
  • Multiparadigmas para concorrência e paralelismo: locks, memória transacional, mensageria, filas, tópicos, paralelismo grátis com balanceamento de carga automático e threads baratas.

É fácil notar que Java já percebeu sua deficiência e busca soluções com as liberações novas. Neste ano será feita a liberação do Java 8 com suporte a mecanismos funcionais (projeto Lambda). A Apache decidiu não esperar pela nova versão e já têm a biblioteca Functor para tentar suprir estas necessidades. Assim também fez o Google com adições funcionais à biblioteca Guava.

Para a JVM já existem algumas linguagens tentando manter a plataforma aderente ao novo modelo de computação, por exemplo: Clojure, Groovy e Scala.

E você, está se preparando? Se você ainda não experimentou nenhuma linguagem funcional, te aconselho a ir logo conhecer o que este paradigma oferece. Seguindo o paradoxo de Blub, o ideal é começar com uma linguagem funcional pura, como Haskell, antes de avaliar linguagens como Scala que são híbridas e oferecem o paradigma tradicional.

15 de maio de 2012

Haskell Chat

Este post apresentará um programa simples de bate-papo por Telnet na linguagem Haskell.

Este programa servirá como um servidor de bate-papo, bastando que as pessoas conectem nele para que possam trocar mensagens entre si. As pessoas não precisarão conhecer onde os outros participantes do bate-papo estão na rede, o servidor se encarregará de redirecionar cada mensagem. Ele atuará parecido com um roteador.

As informações trafegadas na rede serão texto puro, tanto do cliente quanto do servidor, sendo assim não é necessário um cliente especial para utilização do serviço de bate-papo. Os clientes poderão se conectar ao servidor utilizando o Telnet.

Cabeçalho

Nosso servidor de bate-papo será um módulo Haskell projetado para rodar como um aplicativo O nome deste módulo será Main:

module Main(main) where

Imports

Para que o bate-papo funcione e possamos enviar informações pela rede, precisamos de acesso à camada de rede do sistema operacional:

import Network

Além disto, precisaremos utilizar controles sobre efeitos colaterais para que possamos atender mais de um usuário ao mesmo tempo:

import System.IO
import Control.Monad
import Control.Concurrent

Para que nosso servidor não fique apenas como um mediador de mensagens, faremos alguns tratamentos aos textos recebidos pelos usuários, por exemplo a utilização de alguns comandos, para isto precisaremos manipular Strings, que nada mais são do que listas de caracteres:

import Data.List

Tipos

Para controlar as informações que serão armazenadas e trafegadas. Iremos definir alguns tipos para nos ajudar a realizar este controle.

Mensagens

As mensagens trocadas entre usuários nada mais são do que Strings, então esta será nossa definição de mensagem:

type Message = String

Usuários

Será necessário dinstinguir quem mandou cada mensagem para saber para quem repassar cada mensagem, evitando que o próprio usuário receba sua própria mensagem e para que o bate-papo faça sentido.

Para isto precisaremos de um tipo que represente um usuário do bate-papo e que seja possível compará-lo com outros usuários. Este usuário será identificado por um nome e, para que seja possível distinguir usuários com o mesmo nome, por um identificador único.

data ChatUser = ChatUser { 
    userId :: Int, 
    username :: String }
        deriving Eq

Mensagens de bate-papo

A mensagem que recebemos pela rede é texto puro, mas precisamos associá-la ao usuário que fez o envio. Por isto temos um tipo que vincula uma mensagem a um usuário.

data ChatMessage =
    ChatMessage { 
        messageFrom :: ChatUser, 
        messageText :: Message }

E, para que seja possível notificar sobre a entrada ou saída de algum usuário, iremos declarar também outros dois tipo de mensagens do bate-papo: as mensagens de saída e as mensagens de entrada. As mensagens de saída poderão ter algum texto de despedida do usuário.

| QuitMessage { 
        messageFrom :: ChatUser,
        messageText :: Message }
    | JoinMessage {
        messageFrom :: ChatUser }

Instâncias

Como o servidor irá transmitir apenas texto puro, ao receber uma mensagem de um usuário e repassá-la a outro, será necessário exibir o usuário que enviou a mensagem de forma textual. Para isto declaramos uma instância de Show para nosso tipo ChatUser:

instance Show ChatUser where
    show (ChatUser uid name) = 
        name ++ "(" ++ show uid ++ ")"

Também será necessário trafegar os diferentes tipos de mensagens na forma de texto puro pela rede:

instance Show ChatMessage where
    show (ChatMessage user message) =
        show user ++ ": " ++ message
    show (QuitMessage user message)
        | null message =
            "! " ++ show user ++ " saiu."
        | otherwise =
            "! " ++ show user ++ " saiu: " ++ message
    show (JoinMessage user) =
        "! " ++ show user ++ " entrou."

Main

Porta de comunicação

Nosso bate-papo tem como principal objetivo funcionar via Telnet, então para isto faremos com que o servidor escute na porta 23, que é a porta padrão utilizada pelo Telnet.

port :: PortID
port = PortNumber 23

Identificação dos usuários

O identificador único de cada usuário será determinado pela ordem de chegada no servidor, i.e. o primeiro usuário terá o identificador 1, o segundo terá o identificador 2, e assim por diante.

Suportando múltiplos usuários

Para que nosso servidor não fique travado lidando com apenas um usuário, iremos criar Threads para lidar com cada usuário de forma concorrente.

Ao receber uma mensagem de um usuário, a Thread responsável por aquela conexão terá que repassar esta informação às demais, para isto utilizaremos um canal de envio e recebimento de mensagens que será compartilhado entre todas as Threads.

Este canal funciona como um tópico, quando alguém escreve nele, todos que estiverem escutando irão receber a mensagem. Assim será possível que a Thread lidando com outro usuário receba esta mensagem através deste canal de comunicação e repasse ao cliente.

Porém, se utilizarmos apenas uma Thread por cliente, esta só teria a chance de ler deste canal de comunicação depois do cliente enviar alguma mensagem. Pois a Thread ficaria bloqueada esperando por uma escrita no Socket.

Para evitar este problema, serão utilizadas duas Threads por cliente: uma responsável pelo recebimento de mensagens e outra responsável pelo envio.

Main

Nosso servidor iniciará definindo o número inicial de conexões (userCount), criando um canal de comunicação (chan), abrindo um Socket para escutar na porta definida anteriormente (socket) e aguardando conexões.

main :: IO ()
main = withSocketsDo $ do
         let userCount = 0
         chan <- newChan
         socket <- listenOn port
         handleConnections socket userCount chan

Aceitando conexões

Ao receber uma conexão, precisamos manter o handle para realizar a leitura e escrita de mensagens. Desligamos o buffer para que as mensagens sejam trocadas em tempo real.

Após definida a conexão, precisamos liberar nosso servidor para que possa atender outros usuários, então será criado um processo concorrente que atenderá esta conexão. O identificador único do usuário será definido no processo principal para evitar que dois usuários tenham o mesmo identificador.

handleConnections :: Socket -> 
                     Int -> 
                     Chan ChatMessage ->
                     IO ()
handleConnections socket userCount chan = do
    (socketHandle, _, _) <- accept socket
    hSetBuffering socketHandle NoBuffering
    let nextUser = (userCount + 1)
    _ <- forkIO $
      handleUserConnection chan socketHandle nextUser
    handleConnections socket nextUserNumber chan

Atendendo uma conexão

Para iniciar o atendimento à uma conexão, precisamos inicialmente do nome do usuário.

handleUserConnection :: Chan ChatMessage ->
                        Handle ->
                        Int ->
                        IO ()
handleUserConnection chan socketHandle userNumber = do
    name <- readUsername socketHandle

Com o nome do usuário e o identificador único fornecido pelo processo principal do servidor, conseguimos definir o usuário desta conexão e enviar uma mensagem ao canal de comunicação informando que o usuário entrou no bate-papo.

let thisUser = ChatUser userNumber name
    broadcast chan $ JoinMessage thisUser

Para que este usuário comece a receber as mensagens do bate-papo, criamos uma Thread separada para efetuar a leitura do canal de comunicação:

chanReader <- dupChan chan
    readerThread <- forkIO $
      startReader chanReader socketHandle thisUser

Este processo irá efetuar a escrita do canal de comunicação a partir das mensagens recebidas pelo Socket do cliente. O resultado deste processo, será uma mensagem de saída do usuário, caso ele deixe alguma. Caso ele desconecte sem deixar nenhuma mensagem, a exceção será capturada e convertida em uma mensagem vazia de despedida do usuário.

quitMessage <- (startSender chan 
                            socketHandle thisUser)
                            `catch` (\_ -> return "")

Com a mensagem de despedida em mãos, informamos os demais usuários que este deixou o bate-papo. Encerramos a Thread responsável por fazer a leitura do canal de comunicação e fechamos a conexão com o cliente:

broadcast chan $ QuitMessage thisUser quitMessage
    killThread readerThread
    hClose socketHandle

Recebendo o nome do usuário

Para receber o nome do usuário, apenas enviamos uma mensagem pedindo que ele informe seu nome e fazemos a leitura do Socket:

readUsername :: Handle -> IO String
readUsername socketHandle = do
    hPutStr socketHandle "Informe seu nome: "
    readLine $ socketHandle

Recebendo mensagens do canal de comunicação

O processo responsável por receber as mensagens do canal de comunicação só será encerrado com a desconexão do usuário, então ele sempre deve estar lendo do canal e publicando ao usuário. O próprio usuário irá enviar mensagens para o canal de comunicação, para evitar que ele receba mensagens dele mesmo, é aplicado um filtro para que só sejam exibidas mensagens de usuários diferentes dele.

startReader :: Chan ChatMessage ->
               Handle ->
               ChatUser ->
               IO ()
startReader chanReader socketHandle thisUser =
    forever $ do
        message <- readChan chanReader
        when ((messageFrom message) /= thisUser) $
            display message socketHandle

Enviando mensagens ao canal de comunicação

O processo responsável por enviar mensagens ao canal de comunicação deverá terminar de alguma forma. Seja por desconexão do usuário ou por uma mensagem de despedida do mesmo.

Para que o usuário saia do bate-papo, basta que ele envie uma mensagem iniciando com /quit seguido de sua mensagem de despedida. As demais mensagens recebidas do usuário serão repassadas ao canal de comunicação e não encerrarão o processo.

startSender :: Chan ChatMessage ->
               Handle ->
               ChatUser ->
               IO String
startSender chan socketHandle thisUser = do
    line <- readLine socketHandle
    if "/quit" `isPrefixOf` line
        then return . dropWhile (== ' ') $ drop 5 line
        else do
             broadcast chan $
                 ChatMessage thisUser line
             startSender chan socketHandle thisUser

Utilitários

Notificando os processos responsáveis por cada usuário

Para enviar uma mensagem a todos os usuários, basta realizar uma escrita no canal de comunicação.

broadcast :: Chan ChatMessage -> ChatMessage -> IO ()
broadcast = writeChan

Enviando mensagens para um usuário

Para enviar uma mensagem a um usuário, escrevemos a mensagem no Socket seguida de uma quebra de linha no formato Windows, para que a utilização via Telnet seja agradável.

display :: ChatMessage -> Handle -> IO ()
display message socketHandle = do
    hPutStr socketHandle (show message ++ "\r\n")
    hFlush socketHandle

Lendo mensagens de um usuário

Para ler uma mensagem do usuário, lemos uma linha do Socket e removemos o retorno do carro, para que fique apenas a mensagem escrita por ele.

readLine :: Handle -> IO String
readLine = liftM (filter (/= '\r')) . hGetLine

Código completo

O código completo está disponível no GitHub com a descrição Sample telnet chat in Haskell.