Páginas

2013-10-29

Transformações de Dados - em Haskell

by Marcelo Parrela - marcelo.parrela@gmail.com

Ideia Inicial

Deparei-me com uma ideia interessante: representar os elementos químicos, da tabela periódica, em Haskell. Pensei em fazer uma aplicação que carregue os dados de um arquivo CSV (com campos separados por ';'), que converta os dados para uma estrutura de dados do tipo tupla e, finalmente, que processe os dados dessas tuplas.
Não me prenderei aos tipos de processamentos que serão possíveis - até mesmo porque não tenho isso claro, em mente, ainda. Mas, gostaria de falar sobre a carga do arquivo e de como serão feitas as transformações dos dados.
Linguagens funcionais trabalham a transformação de dados de uma maneira muito direta; chega a ser impressionante a facilidade de se lidar com esse tipo de problema - usando Haskell (também, Lisp). Nota: Não conheço outras linguagens funcionais, mas, acredito que em Scala e em Clojure teremos resultados muito interessantes também.

Estrutura de Dados

Consegui informações básicas sobre todos os elementos da tabela periódica - em sites, na Intenet. São elas: sigla, nome, número atômico, massa atômica, número do grupo e número do período. Reuní tudo e montei um arquivo CSV, com os campos sendo separados por ';'. Veja o modelo abaixo:
Dado:
    sigla;nome;número atômico;massa atômica;grupo;período

Onde:
    Sigla e Nome são do tipo String; 
    Número atômico, Grupo e Período são do tipo Int; 
    Massa atômica é do tipo Double.
Decidi utilizar apenas uma tupla para representar um elemento químico. Os dados do arquivo CSV serão mapeados para uma lista cujos elementos são desse tipo de tupla. É importante salientar que existem outras formas de representar dados em Haskell.
Nota: Para quem não está acostumado com programação funcional, tupla é uma estrutura de dados muito simples, assemelhando-se ao record do Pascal ou à struct de C.
Essa tupla possuirá a seguinte estrutura:
( String, String, Int, Double, Int, Int )

O Processamento

    Observações:
  • Caro leitor, procurei descrever a carga e o processamento do CSV de forma didática - para que as pessoas que não possuem familiaridade com a Linguagem Haskell consigam compreender a solução proposta.
  • Não me preocupei com tratamento de exceção. O processamento é feito levando-se em consideração que tudo irá bem.
  • Você poderá baixar os códigos e o CSV na sessão Downloads, ao final do artigo.
Primeiramente, a leitura do arquivo será feita pela função readFile - que executa uma leitura não bufferizada do arquivo. Esta função retorna o conteúdo do arquivo carregado em uma única String. Exemplo:
# rodando em shell - criando o arquivo de exemplo
echo "Ac;Actínio;89;227;3;7" > linhas.txt
echo "Ag;Prata;47;107.8682;11;5" >> linhas.txt
echo "Al;Alumínio;13;26.9815386;13;3" >> linhas.txt
-- em Haskell - lendo o arquivo de exemplo
a <- readFile "linhas.txt"
print a
"Ac;Actínio;89;227;3;7\nAg;Prata;47;107.8682;11;5\nAl;Alumínio;13;26.9815386;13;3\n"
Nesse ponto, nota-se que os dados estão separados pelo caractere de quebra de linha '\n'. Para separar os dados, utilizaremos função lines que quebra uma String em uma lista de Strings usando o caractere de quebra de linha como delimitador. Veja o exemplo:
a <- readFile "linhas.txt"
let linhas = lines a
print linhas 
["Ac;Actínio;89;227;3;7","Ag;Prata;47;107.8682;11;5","Al;Alumínio;13;26.9815386;13;3"]
Agora, iteramos cada elemento da lista usando a função map e aplicamos - a cada elemento - uma função de transformação. Esta função, por sua vez, separará os campos do CSV e os aplicará à tupla.

A função map recebe - como parâmetro - uma lista e uma função transformadora que processará os elementos dessa lista. Daí, ela itera os elementos da lista e executa a função para cada um deles. O resultado final é uma lista que resultou do processamento da original. Exemplo:
-- A função 'odd' recebe um número e retorna True se for ímpar e False se for par.
let lista = map (odd) [1,2,3,4,5,6,7,8,9,10] 
print lista
[True,False,True,False,True,False,True,False,True,False]
Observação: Note que a função map é usada exclusivamente para transformar uma lista. E, no exemplo acima, foi usada para transformar uma lista de números inteiros em uma outra lista de valores booleanos - resultantes da aplicação da função odd.

Retornando ao problema proposto neste artigo: para cada elemento da lista de Strings, devemos separar os campos (delimitados por ';') e convertê-los para a nova estrutura. Porém, não conheço, em Haskell, uma função que faça o split pelo caractere ';'. Ou seja, que seja parametrizável. E, sinto muita vergonha em ter que reconhecer isso! Eu já pesquisei em várias fontes, porém, não encontrei a tal função.
Curiosamente, em Haskell, existem diversas funções que executam algum tipo de split. Tais como lines, que já foi cidada. Outra função é a words - que recebe uma String e retorna uma lista cujos elementos são as palavras da String original (obs.: esta função não é adequada ao nosso problema). Mas, uma bendita função split que seja parametrizável, eu ainda não achei não! :-(
Foi exatamente por esse motivo que tive de escrever minha própria função split. Veja o código abaixo:
-- by Marcelo Parrela - marcelo.parrela@gmail.com
split :: (Eq a) => a -> [a] -> [[a]]
split _ [] = []
split c as = w : split c r
    where 
        (w,r) = split' c as []

        split' _ [] l1 = (l1,[])
        split' c (a:as) l1
            | a /= c = split' c as (l1++[a])
            | otherwise = (l1,as)
Com a função split, poderemos testar a separação dos campos CSV. Exemplo:
split ';' "Ac;Actínio;89;227;3;7"
["Ac","Actínio","89","227","3","7"]
Veja o resultado da função split em conjunto com a função map:
map (split ';') [ "Ac;Actínio;89;227;3;7" , "Ag;Prata;47;107.8682;11;5" , "Al;Alumínio;13;26.9815386;13;3" ]
[ ["Ac","Actínio","89","227","3","7"], ["Ag","Prata","47","107.8682","11","5"], ["Al","Aluminio","13","26.9815386","13","3"] ]    
Bem, já possuímos os dados dos elementos químicos em um formato muito mais amigável para se trabalhar. Basta-nos, agora, aplicar uma segunda função de transformação para converter os elementos em tuplas.
let lista = [ ["Ac","Actínio","89","227","3","7"], ["Ag","Prata","47","107.8682","11","5"], ["Al","Aluminio","13","26.9815386","13","3"] ]
    
let toTupla = (\[si,no,nu,ma,gr,pe] -> (si, no, read nu :: Int, read ma :: Double, read gr :: Int, read pe :: Int) )
    
let listaFinal = map toTupla lista 
print listaFinal
[ ("Ac","Actínio",89,227.0,3,7) , ("Ag","Prata",47,107.8682,11,5) , ("Al","Aluminio",13,26.9815386,13,3) ]
Para deixar mais claro, o código, informo que a função read converte um valor de String para outro tipo de dado. E, a aplicação do cast usando :: Int e ::Double apenas especifica o tipo de dado de destino dessa conversão.
A partir daqui, possuímos os dados em formatos de fácil processamento e poderemos elaborar cálculos diversos.

Montando o Quebra-Cabeças

Agora, vamos ver como fica tudo junto:
toTupla [si,no,nu,ma,gr,pe] = (si, no, read nu ::Int, read ma ::Double, read gr ::Int, read pe ::Int)

main = do
    dados <- readFile "./elementos-quimicos.csv"

    let elementos = map toTupla $ map (split ';') $ lines dados
    
    print elementos
    ...
Acredito que é possível notar a simplicidade e a clareza de código. Ficou um código limpo, pequeno e de fácil manutenção.

Downloads



Nenhum comentário:

Postar um comentário