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