sábado, 25 de junho de 2011

De volta aos álbuns

Hoje atualizei meu Eclipse para o Indigo e resolvi instalar o plugin counterclockwise - plugin para projetos Clojure no Eclipse. Gostei da diferenciação entre níveis de parênteses e resolvi então deixar um pouco o TextMate de lado. Mas e como importar para o Eclipse o meu projeto criado com o lein?

Pois o lein possui um plugin para isto. Basta adicionar a dependência do plugin no projeto:



(defproject a-thousand-things-to-do "1.0.0-SNAPSHOT"
:description "My first experiments with Clojure"
:dependencies [[org.clojure/clojure "1.2.1"]
[clojure-csv "1.2.4"]]
:main a-thousand-things-to-do.core)
:dev-dependencies [[lein-eclipse "1.0.0"]]


e invocar "lein deps" seguido de "lein eclipse" via linha de comando.

quarta-feira, 22 de junho de 2011

Fugindo dos álbuns

De tempos em tempos nos reunimos no trabalho para incentivar práticas de programação e design através de Dojos, e nos próximos dias teremos um Coding Dojo para exercitar desenvolvimento em Java, e o problema que apresentaremos será decompor um valor em cédulas de uma determinada moeda. A prática do Dojo será evolutiva, partindo do problema inicial como sendo a decomposição de valores inteiros em cédulas inteiras, evoluindo para o tratamento de centavos e finalizando com um controle de cédulas disponíveis. Resolvi praticar este problema individualmente, com Clojure (claro) e de início cheguei na seguinte implementação do problema inicial:

(ns troco)

(def cedulas-reais { :cem 100, :cinquenta 50, :vinte 20, :dez 10, :cinco 5, :dois 2, :um 1 })

(defn decompor [montante cedulas]
(loop [ total-em-cedulas {}
valor-restante montante
notas (seq cedulas)]
(if (not-empty notas)
(let [ face ((first notas) 0),
quanto-vale ((first notas) 1) ]
(recur (assoc total-em-cedulas face (quot valor-restante quanto-vale))
(mod valor-restante quanto-vale)
(rest notas)))
total-em-cedulas)))

(defn decompor-em-cedulas [valor moeda]
(filter #(> (% 1) 0) (seq (decompor valor moeda))))

;(decompor-em-cedulas 37 cedulas-reais)



Desta minha solução, tenho alguns pontos a melhorar
1 - O uso de seq para transformar o map em seqüência de vetores "cédula x valor" é necessário?
2 - Ao invés de declarar variáveis auxiliares (let) para obter a cédula e o valor, não seria melhor criar uma função ou então algum tipo de dado?
3 - Para ignorar as cédulas não utilizadas a melhor forma é utilizar um filter sobre - novamente - um map convertido em seqüência?


(questão 1 resolvida)


(ns troco)

(def cedulas-reais { :cem 100, :cinquenta 50, :vinte 20, :dez 10, :cinco 5, :dois 2, :um 1 })

(defn decompor [montante cedulas]
(loop [ total-em-cedulas {}
valor-restante montante
notas cedulas]
(if (not-empty notas)
(let [ face ((first notas) 0),
quanto-vale ((first notas) 1) ]
(recur (assoc total-em-cedulas face (quot valor-restante quanto-vale))
(mod valor-restante quanto-vale)
(rest notas)))
total-em-cedulas)))

(defn decompor-em-cedulas [valor moeda]
(filter #(> (% 1) 0) (decompor valor moeda)))


Hoje li que a função (first) já opera sobre maps como sendo seqs, retornando um vetor com o primeiro key/value deste.

(questão 2 respondida)


(ns troco)

(def cedulas-reais { :cem 100, :cinquenta 50, :vinte 20, :dez 10, :cinco 5, :dois 2, :um 1 })

(defn face [cedula]
(cedula 0))

(defn valor [cedula]
(cedula 1))

(defn decompor [montante cedulas]
(loop [ total-em-cedulas {}
valor-restante montante
notas cedulas]
(if (not-empty notas)
(let [ cedula (first notas)]
(recur (assoc total-em-cedulas (face cedula) (quot valor-restante (valor cedula)))
(rem valor-restante (valor cedula))
(rest notas)))
total-em-cedulas)))

(defn decompor-em-cedulas [valor moeda]
(filter #(> (% 1) 0) (decompor valor moeda)))


Não sei dizer se fez diferença em termos de clareza: acabou por ficar um (let) para referenciar a cédula topo do map, e as duas funções para retornar o primeiro e segundo elementos do vetor key/value, respectivamente.

Sobre utilizar algum tipo de dado, até pensei em tornar o map em um vetor de maps, composto por duas chaves. Algo assim:


(ns troco)

(def cedulas-reais [{:face :cem, :valor 100}
{:face :cinquenta, :valor 50}
{:face :vinte, :valor 20}
{:face :dez, :valor 10}
{:face :cinco, :valor 5}
{:face :dois, :valor 2}
{:face :um, :valor 1}])

(defn decompor [montante cedulas]
(loop [ total-em-cedulas {}
valor-restante montante
notas cedulas]
(if (not-empty notas)
(let [ cedula (first notas)]
(recur (assoc total-em-cedulas (cedula :face) (quot valor-restante (cedula :valor)))
(rem valor-restante (cedula :valor))
(rest notas)))
total-em-cedulas)))

(defn decompor-em-cedulas [valor moeda]
(filter #(> (% 1) 0) (decompor valor moeda)))


Mas não sei ainda afirmar o que parece "mais correto" em clojure =/

Ah, sutilmente =) troquei (mod) por (rem). Puramente por questões sintáticas.

(questão 3 respondida)

(ns troco)

(def cedulas-reais [{:face :cem, :valor 100}
{:face :cinquenta, :valor 50}
{:face :vinte, :valor 20}
{:face :dez, :valor 10}
{:face :cinco, :valor 5}
{:face :dois, :valor 2}
{:face :um, :valor 1}])

(defn decompor-em-cedulas [valor cedulas]
(loop [ total-em-cedulas {}
valor-restante valor
notas cedulas]
(if (not-empty notas)
(let [ cedula (first notas)]
(if (>= valor-restante (cedula :valor))
(recur (assoc total-em-cedulas (cedula :face) (quot valor-restante (cedula :valor)))
(rem valor-restante (cedula :valor))
(rest notas))
(recur total-em-cedulas
valor-restante
(rest notas))))
total-em-cedulas)))


Não me passou pela cabeça que pudesse colocar 2 (recur) mas faz sentido, já que o (recur) é uma substituição à chamada recursiva da 'função' definida como (loop). Só não estou gostando de ter estes dois (if), e ainda me pergunto se não dá pra resolver com somente um (recur), mas continuarei pesquisando.

Por hoje era isto ;-)

terça-feira, 7 de junho de 2011

Usando map para formar... maps

Minha primeira function =)

Map necessita de uma função unária e uma sequence de dados para serem transformados pela função.

Seu uso seria algo assim então:


(map as-album-map sequence-of-albums)


a definição de as-album-map poderia então ser assim:


(defn as-album-map [album-data]
{ :year (get album-data 0)
:title (get album-data 1)
:artist (get album-data 2) })


e sequence-of-albums pode ser uma sequence definida a partir da função load-album-sequence:


(defn load-album-sequence [file-name]
(binding [*delimiter* \;] (parse-csv (char-seq (reader file-name))))))

(def sequence-of-albums (load-album-sequence "resources/1001_Albums.csv"))


Alterando então o arquivo core.clj do projeto, fica exatamente assim:


(ns a-thousand-things-to-do.core)
(use '[clojure.java.io :only (reader)])
(use '[clojure-csv.core])

(defn as-album-map [album-data]
{ :year (get album-data 0)
:title (get album-data 1)
:artist (get album-data 2) })

(defn load-album-sequence [file-name]
(binding [*delimiter* \;]
(parse-csv (char-seq (reader file-name)))))


Alterei project.clj para apontar core como o arquivo principal. Ficou assim:


(defproject a-thousand-things-to-do "1.0.0-SNAPSHOT"
:description "My first experiments with Clojure"
:dependencies [[org.clojure/clojure "1.2.1"]
[clojure-csv "1.2.4"]]
:main a-thousand-things-to-do.core)


Carregando para o REPL

crisweber$ lein repl


E finalmente testando:


(map as-album-map (load-album-sequence "resources/1001_Albums.csv")


e funcionou =)

Updated 7 jun 2001 ===> SVN Atualizado!

Estruturando a lista de álbums

So far, so good, mas por enquanto não saí da digitação de código no REPL. Percebi que o ideal é incluir no projeto uma função que retorne a lista de álbums para utilizá-la como origem dos dados para outras funções. Mas para conseguir manipular os álbums preciso identificar de forma estruturada cada uma das colunas que tenho no arquivo. Quem sabe um vector composto de vários maps identificando cada álbum. Tenho no arquivo o Ano de Lançamento, o Título do Álbum e o Artista... Uma representação como abaixo deve bastar:

{:year 1955
:title "In The Wee Small Hours"
:artist "Frank Sinatra" }


Agora, como converter uma sequence de vectors em um vector de maps? Resolvi ler as formas de iteração apresentadas na seção 2.3 do Clojure In Action. Três possibilidades surgiram:

  • Usar map
  • Usar loop/recur
  • Usar doseq

Decidi então tentar com as três pra ver no que dá =)

quarta-feira, 1 de junho de 2011

Carregando o conteúdo de 1001_Albums.csv

No REPL


crisweber$ lein repl
REPL started; server listening on localhost:55881.


Resolvi verificar se o parse do arquivo resources/1001_Albums.csv retorna os 1001 álbuns que supostamente deveríamos ouvir antes de morrer:


user=> (use '[clojure.java.io :only (reader)])
nil
user=> (use '[clojure-csv.core])
nil
user=> (= 1001 (count (binding [*delimiter* \;] (parse-csv (char-seq (reader "resources/1001_Albums.csv"))))))
true


É, 1001 e contando!

Primeiro teste com clojure-csv

Encontrei arquivo de testes do clojure-csv um exemplo bem simples para testar se até aqui está tudo Ok:

test_csv.clj

Iniciei então o REPL


crisweber$ lein repl
REPL started; server listening on localhost:51550.


e inseri o seguinte código


user=> (ns test-csv
(:use clojure-csv.core))
nil
test-csv=> (parse-csv "a,b,c")
(["a" "b" "c"])


e funcionou =)

Só que gerei meu arquivo .csv usando ";" como delimitador...

Parece que o clojure-csv suporta mudança de delimitador definindo na variável *delimiter*.

Testando:


test-csv=> (binding [*delimiter* \;] (parse-csv "a;b;c"))
(["a" "b" "c"])


Funcionou também! =D

clojure-csv para leitura de arquivos CSV

Um pouco de pesquisa no Google e descobri este projeto: clojure-csv disponível no clojars.org - que é um repositório maven para projetos clojure utilizado pelo leiningen.

Alterei o arquivo project.clj para inserir a nova dependência:


(defproject a-thousand-things-to-do "1.0.0-SNAPSHOT"
:description "FIXME: write description"
:dependencies [[org.clojure/clojure "1.2.1"]
[clojure-csv "1.2.4"]])


e atualizei as dependências do projeto via leiningen:


crisweber$ lein deps
Downloading: clojure-csv/clojure-csv/1.2.4/clojure-csv-1.2.4.pom from central
Downloading: clojure-csv/clojure-csv/1.2.4/clojure-csv-1.2.4.pom from clojure
Downloading: clojure-csv/clojure-csv/1.2.4/clojure-csv-1.2.4.pom from clojars
Transferring 1K from clojars
Downloading: org/clojure/clojure/1.2.0/clojure-1.2.0.pom from clojure
Transferring 1K from clojure
Downloading: org/clojure/clojure-contrib/1.2.0/clojure-contrib-1.2.0.pom from clojure
Transferring 4K from clojure
Downloading: clojure-csv/clojure-csv/1.2.4/clojure-csv-1.2.4.jar from central
Downloading: clojure-csv/clojure-csv/1.2.4/clojure-csv-1.2.4.jar from clojure
Downloading: clojure-csv/clojure-csv/1.2.4/clojure-csv-1.2.4.jar from clojars
Transferring 5K from clojars
Downloading: org/clojure/clojure-contrib/1.2.0/clojure-contrib-1.2.0.jar from clojure
Transferring 466K from clojure
Copying 3 files to /Users/crisweber/Documents/Projects/1001-things-to-do/lib


A formatação sintática pra Clojure eu encontrei aqui: All Syntax Highlighter 2.0 brushes collected, described and downloadable

Errando e aprendendo!

Ao tentar abrir a console REPL do Clojure pela primeira vez no novo projeto

crisweber$ lein repl

descobri que o nome 1001-things-to-do foi utilizado dentro dos fontes como namespaces, e Clojure exige que namespaces, funções, variáveis, etc iniciem com letras.

Alterei o conteúdo dos arquivos core.clj existentes em /src e /test, alterei o conteúdo de project.clj na raíz e alterei o nome dos subdiretórios /src/1001_things_to_do e /test/1001_things_to_do para /*/a_thousand_things_to_do.

crisweber$ svn move 1001_things_to_do a_thousand_things_to_do

Commit realizado, problemas corrigidos.

Executei os testes gerados pelo leiningen para confirmar que as mudanças de nomes ocorreram com sucesso:

crisweber$ lein test
Testing a-thousand-things-to-do.test.core
FAIL in (replace-me) (core.clj:6)
No tests have been written.
expected: false
actual: false
Ran 1 tests containing 1 assertions.
1 failures, 0 errors.

Agora parece estar Ok!! =)

Criando o projeto com Leiningen

Hora de fazer algo :-)

Comando simples para criar o projeto:

crisweber$ lein new 1001-things-to-do
Created new project in: /Users/crisweber/Documents/Projects/1001-things-to-do

Adicionar o projeto ao controle de versão:

crisweber$ svn checkout https://1001-things-to-do.googlecode.com/svn/trunk/ 1001-things-to-do --username cris.weber@gmail.com
Checked out revision 1.

Adicionar o conteúdo do projeto ao controle de versão:

crisweber$ svn add *
A README
A project.clj
A src
A src/1001_things_to_do
A src/1001_things_to_do/core.clj
A test
A test/1001_things_to_do
A test/1001_things_to_do/test
A test/1001_things_to_do/test/core.clj

Versionar tudo!

crisweber$ svn commit -m "First commit"
Authentication realm: Google Code Subversion Repository
Password for 'crisweber':
Adding README
Adding project.clj
Adding src
Adding src/1001_things_to_do
Adding src/1001_things_to_do/core.clj
Adding test
Adding test/1001_things_to_do
Adding test/1001_things_to_do/test
Adding test/1001_things_to_do/test/core.clj
Transmitting file data ....
Committed revision 2.

Sobre o password, o Google Code disponibiliza um link para geração de uma senha pessoal para o SVN na aba Source / Checkout.

A formatação dos códigos acima foi feita seguindo este tutorial:

Feito isto, bastou adicionar um diretório /resources na raíz do projeto e lá copiar o arquivo 1001_Albums.csv (link para o arquivo no trunk do projeto)

Compartilhando o trabalho

Pensei um pouco em como compartilhar os arquivos que criei e alguns resultados, e decidi criar um projeto no Google Code. Na página de hosting do Google Code existe um link 'Create a new project' onde toda a mágica acontece.

Por mágica entendam que basta descrever o projeto, selecionar o tipo de sistema de versionamento e a licença a ser utilizada.

Como estou BEM mais acostumado com SVN optei por utilizá-lo, ao menos inicialmente. Já a licença é algo que eu não havia pensado anteriormente :-) então peguei a de nome mais extenso (Mozilla Public License 1.1).

E pronto!

Projeto criado: 1001-things-to-do

Decisões...

Decidi que a primeira coisa a fazer com o conjunto de dados dos 1001 álbuns é ler o conteúdo do arquivo CSV utilizando Clojure.

Meu primeiro contato com Clojure foi através do livro Seven Languages in Seven Weeks

Seven Languages in Seven Weeks: A Pragmatic Guide to Learning Programming Languages (Pragmatic Programmers) by Bruce A. TateSeven Languages in Seven Weeks: A Pragmatic Guide to Learning Programming Languages (Pragmatic Programmers)

e decidi estudar um pouco mais, adquirindo também o livro Clojure in Action (comprei o ebook no Manning MEAP)

Clojure in Action by Amit RathoreClojure in Action

Para simplificar um pouco a criação de um projeto onde organizar os fontes, bibliotecas e recursos vou utilizar o Leiningen (utilitário para construção de projetos, escrito em Clojure). Utilizei este guia para instalação: Using Leiningen to build Clojure code



Obtendo a relação dos 1001 álbuns para ouvir antes de morrer

Obtive no site 1001 Series a lista dos 1001 álbuns para ouvir antes de morrer. Esta lista é detalhada no livro 1001 Albums you must hear before you die

1001 Albums You Must Hear Before You Die: Revised and Updated Edition by Robert Dimery1001 Albums You Must Hear Before You Die: Revised and Updated Edition

Montei (manualmente) uma planilha com o ano de gravação, nome do álbum e artista, e exportei esta planilha como arquivo CSV. Agora é só começar a brincar =)