Rails Summit Latin America 2009 - O evento
November 17th, 2009
Ano passado estive na primeira edição do Rails Summit Latin America e, dos eventos que participei, foi sem dúvida um dos melhores (veja esse post do Fábio Akita sobre o evento. Este ano o Akita e a Locaweb estavam de volta com o evento que, mais uma vez, acredito ter agradado a maioria dos participantes, sendo considerado por muitos (inclusive por mim) o melhor evento do ano na área.
O que me agrada bastante neste evento é que ele compreende palestras técnicas com algumas mais filosóficas. Contou com palestrantes brasileiros e estrangeiros. Várias das palestras são interessantes mesmo se você não é entusiasta do framework web Ruby on Rails.
Para quem é antenado na comunidade e acompanha outros eventos internacionais, diversos dos temas abordados nas palestras não são novidade. Boa parte da informação passada, você já poderia ter acesso vendo vídeos de outras palestras e acompanhado os blogs dos palestrantes e do mundo de desenvolvimento de software. Mesmo assim, acredito que as palestras são bastante válidas pelo jeito didático em que a informação é passada e que os apresentadores sempre tem algo novo a adicionar além do que já conhecemos.
É sempre bom também encontrar pessoas com interesses semelhantes aos seus e ver o que as empresas estão fazendo em relação ao desenvolvimento de software e ao emprego de diferentes ferramentas, linguagens, metodologias e tecnologias para resolver os problemas que encontramos nessa área.
Gostei muito do evento e espero poder estar presente em ocasiões futuras. Parabéns aos organizadores. O evento deste ano apresentou poucas falhas e fiquei bastante satisfeito.
Também achei positiva a ideia de se alterar o lugar do evento do próximo ano (2010) para o espaço do Frei Caneca, que permitirá manter um bom preço ao evento
Vou listar aqui os pontos positivos e negativos.
Positivos:
Bons palestrantes, assuntos interessantes;
Não faltou comida, banheiros em geral estavam bons;
Não houveram muitos atrasos e as palestras também não foram muito longas;
A desconferência foi bem legal, acharia até que poderia ocorrer nos dois dias;
O espaço estava mais amplo e tinha até videogame para jogar. Apesar que acho que todos que já jogaram Wii, devem ter se cansado do Wii Sports;
O evento foi todo filmado profissionalmente e espero ver em breve os vídeos oficiais na web das palestras paralelas que não pude assistir;
Encontrei muita gente legal, inclusive muitos dos participantes do Guru-SP e muita gente que esteve no evento no ano passado.
Negativos:
Brian Liles não pôde ir. Tenho certeza que seria uma das palestras mais interessantes e divertidas e que ele nos lembraria do TAFT. A organização não teve culpa nisso, mas é sempre ruim ter suas expectativas quebradas.
Alguns problemas de áudio (microfones). Atrapalharam um pouco mais nada que comprometesse muito o entendimento das palestras.
Algumas pessoas reclamaram que os projetores não estavam bem regulados em relação a cor e brilho.
A Sala B esse ano estava maior e com ar condicionado. Ano passado a sala foi um forno. Mesmo assim essa sala não era tão legal. Não tinha tomadas (o que fez com que eu não conseguisse gravar por completo a palestra do Ilya Grigorik). Além disso não gostei da disposição das telas e dos pilares de aço no meio do caminho.
O almoço foi comestível mas meio sem graça. Bem, isso não é um grande problema porque acho que ninguém passou fome.
Café e água não foram oferecidos o dia todo e muita gente ficou sem café durante os coffee breaks.
Os Coffee breaks tinham muitos doces e comidas gordurosas. Doce é bom mas depois de dois dias eu já estava quase passando mal. Gosto da opção de sanduíches de metro. Em geral são mais saudáveis e tem ingredientes mais frescos, além de alimentarem melhor. :)
O estacionamento no Anhembi é muito caro (25 reais) e não havia opções mais baratas. Quem parou no Hotel ali perto e passou das 12 horas desembolsou 50 reais.
Apesar destas críticas, o balanço do evento foi bastante positivo.
Vídeos
Eu filmei quase todas as palestras que aconteceram na sala principal (acho que a única exceção foi a do Carlos Brando). Coloquei os vídeos na minha conta do Blip.tv. E tem dois deles no Viddler. Se você notar que algum vídeo está travando em alguma parte, avise-me para que eu possa fazer o upload.
Livestream
Nesse ano o Livestream do Blogblogs foi superado pelo Trendtime desenvolvido pelo Cairo Noleto. Nestes dois streams é possível acompanhar quase tudo que aconteceu no evento.
Fotos
Tirei algumas fotos do Rails Summit que, no geral, ficaram ruins. O Daniel Plentz registrou boas fotos do evento) e o Levy Carneiro fotografou alguns dos palestrantes. O shadow11, também registrou suas fotos. O flickr da Locaweb tem muitas fotos também.
Se que ver fotos da Morena Open Source, passe no projeto do github, que recebeu mais fotos esse ano.
É, isso. Estarei no DevInSampa e espero registrar os vídeos do evento.
Continuando esse post aqui: http://www.agaelebe.com.br/2009/3/12/traducao-paipr-o-resolvedor-de-problemas-gerais-parte-2
Macacos e bananas
Para que possamos ver o G em GPS há uma razão. Vamos ver se podemos aplicar a nova versão do GPS para um novo domínio. Este é um clássico problema de IA – macacos e bananas. A idéia básica é que há um macaco em frente a porta de uma sala, há bananas penduradas no meio da sala – longe do alcance dos macacos - e há uma cadeira próxima a porta que o macaco pode empurrar para dentro da sala e então subir nela para alcançar as bananas. A primeira coisa que precisamos fazer é definir os operadores para este domínio (lib/ch04/gps2_monkey1.rb):
require 'gps2' require 'pp' $banana_ops = [ GPS2::op(:climb_on_chair, :preconds => [:chair_at_middle_room, :at_middle_room, :on_floor], :add_list => [:at_bananas, :on_chair], :del_list => [:at_middle_room, :on_floor]), GPS2::op(:push_chair_from_door_to_middle_room, :preconds => [:chair_at_door, :at_door], :add_list => [:chair_at_middle_room, :at_middle_room], :del_list => [:chair_at_door, :at_door]), GPS2::op(:walk_from_door_to_middle_room, :preconds => [:at_door, :on_floor], :add_list => [:at_middle_room], :del_list => [:at_door]), GPS2::op(:grasp_bananas, :preconds => [:at_bananas, :empty_handed], :add_list => [:has_bananas], :del_list => [:empty_handed]), GPS2::op(:drop_ball, :preconds => [:has_ball], :add_list => [:empty_handed], :del_list => [:has_ball]), GPS2::op(:eat_bananas, :preconds => [:has_bananas], :add_list => [:empty_handed, :not_hungry], :del_list => [:has_bananas, :hungry]) ]
Nada muito estranho aqui. Então vamos usá-los e ver se o GPS funciona para eles. (lib/ch04/gps2_monkey1.rb):
gps = GPS2.new([:at_door, :on_floor, :has_ball, :hungry, :chair_at_door], $banana_ops) pp gps.solve(:not_hungry) Isto gerará a saída esperada: [[:start], [:executing, :push_chair_from_door_to_middle_room], [:executing, :climb_on_chair], [:executing, :drop_ball], [:executing, :grasp_bananas], [:executing, :eat_bananas]]
E já que não fizemos nenhuma mudança no problema GPS, a afirmação de que ele é geral apresenta alguma validade.
Busca no labirinto
Outro problema clássico de IA é a busca no labirinto. Podemos facilmente definir um labirinto como quadrados numerados onde um quadrado numerado pode levar para zero ou mais outros quadrados. Para fazer as definições realmente fáceis, vamos definir alguns métodos que tornam fácil a definição das operações para nós (lib/ch04/maze.rb):
require 'gps2' require 'pp' module Maze class << self def make_ops(pair) [make_op(pair[0], pair[1]), make_op(pair[1], pair[0])] end def make_op(here, there) GPS2::op([:move, :from, here, :to, there], :preconds => [[:at, here]], :add_list => [[:at, there]], :del_list => [[:at, here]]) end end Ops = [[1,2], [2,3], [3,4], [4,9], [9,14], [9,8], [8,7], [7,12], [12,13], [12,11], [11,6], [11,16], [16,17], [17,22], [21,22], [22,23], [23,18], [23,24], [24,19], [19,20], [20, 15], [15,10], [10,5], [20,25]].inject([]) do |sum, obj| sum + make_ops(obj) end end
Aqui nós temos primeiro makeops, que apenas faz todas as passagens de um quadrado para outro simétricas – o que significa que não teremos que definir uma passagem de 1 para 2 e a de 2 para 1. O método makeop faz uso de algumas propriedades interessantes de nosso código que estavam lá e para as quais nenhuma atenção especial foi dada. Isto é, os objetivos podem ser representados por arrays ao invés de símbolos. De fato, não importa como você representa os objetivos, contanto que você use sempre a mesma representação para o mesmo objetivo. Você pode também misturar e combinar strings, símbolos, regexps e arrays, se quiser. Neste caso, os únicos objetivos que temos são da forma [:at, 1] (em 1), mas é necessário um poder um pouco maior de representação para fazer deste jeito.
No final, um grande array de pares de números é transformado em operações usando inject e fazendo chamadas a make_ops. O resultado final é o conjunto de todas as operações que precisamos para definir esse labirinto em particular. Então, um primeiro problema para este labirinto seria provavelmente saber como sair dele. Então vamos ver isso (lib/ch04/maze_problem1.rb):
require 'blocks' require 'pp' gps = GPS2.new([[:a, :on, :table], [:b, :on, :table], [:space, :on, :a], [:space, :on, :b], [:space, :on, :table]], Blocks.make_ops([:a, :b])) pp gps.solve([:a, :on, :b], [:b, :on, :table])
Isto nos dá a saída esperada:
[[:start], [:executing, [:move, :a, :from, :table, :to, :b]]]
Vamos considerar o caso em que temos a em cima de b e queremos movê-lo de modo que b fique sobre a. Desta vez vamos habilitar o debug de saída (lib/ch04/blocks_problem2.rb):
require 'blocks' require 'pp' gps = GPS2.new([[:a, :on, :b], [:b, :on, :table], [:space, :on, :a], [:space, :on, :table]], Blocks.make_ops([:a, :b])) pp gps.solve([:b, :on, :a])
Isto também nos dá o que esperávamos:
Goal: [:b, :on, :a] Consider: [:move, :b, :from, :table, :to, :a] Goal: [:space, :on, :b] Consider: [:move, :a, :from, :b, :to, :table] Goal: [:space, :on, :a] Goal: [:space, :on, :table] Goal: [:a, :on, :b] Action: [:move, :a, :from, :b, :to, :table] Goal: [:space, :on, :a] Goal: [:b, :on, :table] Action: [:move, :b, :from, :table, :to, :a] [[:start], [:executing, [:move, :a, :from, :b, :to, :table]], [:executing, [:move, :b, :from, :table, :to, :a]]]
Uma das características mais interessantes da minha implementação comparada com a de Norvig é que ele possui alguns problemas de ordenamento que necessitam de código extra no método achieve_all. Eu não entendi porque, mas meu código não tem esse problema, então eu não mostrarei o código consertado (visto que não foi necessário consertá-lo). Um exemplo que causava os problemas para Norvig parece-se com este código aqui (lib/ch04/blocks_problem3.rb):
require 'blocks' require 'pp' gps = GPS2.new([[:a, :on, :b], [:b, :on, :c], [:c, :on, :table], [:space, :on, :a], [:space, :on, :table]], Blocks.make_ops([:a, :b, :c])) pp gps.solve([:c, :on, :b], [:b, :on, :a])
Este problema representa um mundo de blocos onde a está sobre b, b está sobre c e c está sobre a mesa, e você quer reverter a ordem deles.
Outro problema que Norvig tem e que não aparece neste código são alguns exemplos que fazem muitas operações para alcançar algo. O arquivo lib/ch04/blocks_problem4.rb é um deles:
require 'blocks' require 'pp' gps = GPS2.new([[:c, :on, :a], [:a, :on, :table], [:b, :on, :table], [:space, :on, :b], [:space, :on, :c], [:space, :on, :table]], Blocks.make_ops([:a, :b, :c])) pp gps.solve([:c, :on, :table])
Em casos em que a solução ótima precisa de uma ação, o código lisp realiza duas ações (mover c de a para b e então de b para a mesa). Isso acontece pó problemas de ordenação. Uma ordenação topológica (Topological sort) pode resolvê-los, por exemplo. Existe outro exemplo que necessita de quatro operações ao invés de duas (lib/ch04/blocks_problem5.rb):
require 'blocks' require 'pp' gps = GPS2.new([[:c, :on, :a], [:a, :on, :table], [:b, :on, :table], [:space, :on, :b], [:space, :on, :c], [:space, :on, :table]], Blocks.make_ops([:a, :b, :c])) pp gps.solve([:c, :on, :table], [:a, :on, :b])
Mas, como mencionei anteriormente, não parece que o código Ruby tem esse problema, então decidi não implementar as soluções de Norvig para eles.
Interessantemente, existem alguns problemas que não podem ser resolvidos, não importa em qual ordem você ponha os objetivos. Pegue, por exemplo, o caso em que b e a estão na mesa e c está sobre a. Você quer que c fique na mesa, b sobre c e a sobre b. A implementação atual do GPS não pode lidar com isso, não importa em que ordem você tente (lib/ch04/blocks_problem6.rb):
require 'blocks' require 'pp' gps = GPS2.new([[:c, :on, :a], [:a, :on, :table], [:b, :on, :table], [:space, :on, :c], [:space, :on, :b], [:space, :on, :table]], Blocks.make_ops([:a, :b, :c])) pp gps.solve([:a, :on, :b], [:b, :on, :c]) pp gps.solve([:b, :on, :c], [:a, :on, :b])
Isto é chamado de anomalia de Sussman, e algumas soluções serão vistas mais tarde no livro de Norvig.
Conclusão
É óbvio que esta versão do GPS é muito mais poderosa que a original. Mas também existem algumas coisas problemáticas. Embora tenhamos introduzido algumas medidas, ainda há casos em que uma solução correta não pode ser encontrada. Por exemplo, podemos olhar para o problema de pegar uma criança na escola novamente, adicionando um novo operador para pegar um táxi lá. Digamos que queremos pegar o filho na escola sem usar nenhum dinheiro e definimos isso da seguinte maneira (lib/ch04/gps2_problem7.rb):
require 'gps3' require 'pp' gps = GPS2.new([:son_at_home, :have_money, :have_works], GPS::SCHOOL_OPS + [ GPS2::op(:taxi_son_to_school, :preconds => [:son_at_home, :have_money], :add_list => [:son_at_school], :del_list => [:son_at_home, :have_money]) ]) debug :gps pp gps.solve(:son_at_school, :have_money)
Aqui algo engraçado acontece. Se você olha na saída de debug:
Goal: :son_at_school Consider: :drive_son_to_school Goal: :son_at_home Goal: :car_works Consider: :shop_installs_battery Goal: :car_needs_battery Consider: :taxi_son_to_school Goal: :son_at_home Goal: :have_money Action: :taxi_son_to_school Goal: :have_money []
Existem dois modos de resolver isso: primeiro resolver o objetivo de se ter dinheiro (have money) e então pegar o filho na escola. Neste caso, o objetivo have_money é resolvido trivialmente e então o dinheiro é gasto com o táxi. Se outra ordem for tentada então o filho é enviado de táxi para escola e então o objetivo ‘ter dinheiro’ falha. Existe uma solução válida (levar o filho até a escola) mas ela nunca é tentada.
Outro problema com esta abordagem é que o poder descritivo dela não é muito geral nem poderoso o suficiente. As condições que usamos para os operadores no GPS não são realmente abstratas o suficiente.
O GPS também assume que você sabre tudo sobre o mundo (“informação perfeita”), e esse tudo é verdadeiro ou falso. A probabilidade é hoje em dia um das melhores abordagens disponíveis para a AI e o GPS não leva em consideração a lógica fuzzy.
Finalmente, objetivos de interação, em que você tem alguns objetivos ativos ao mesmo tempo é o que a maioria das pessoas tem, mas o GPS só pode receber um objetivo por vez.
Todos esses problemas tornaram problemático continuar usando a abordagem do GPS. Outra coisa que impactou bastante foi à percepção de como problemas NP-completos afetaram resolvedores como o GPS. No final das contas, o GPS foi uma abordagem inicial interessante para resolver o problema, e ele definitivamente faz parte da herança da IA disponível nos dias de hoje.
Tradução - PAIPR - O Resolvedor de Problemas Gerais - parte 2)
March 12th, 2009
Finalmente estou voltando para as traduções dos artigos de Ola Bini. Posso demorar para traduzir, mas espero conseguir traduzir toda a série.
Tradução do seguinte artigo: the general problem solver part 2 Divide em duas partes por ser um artigo grande. A próxima parte você encontra em: http://www.agaelebe.com.br/traducao-paipr-o-resolvedor-de-problemas-gerais-parte-2-continuacao
No último artigo desta série eu descrevi uma simples versão do Resolvedor de Problemas Gerais. Este artigo analisará uma versão mais completa que corrige alguns dos problemas que discuti anteriormente. O primeiro problema a se resolver é que em certos casos pode ser um pouco difícil depurar a saída de nosso resolvedor. Caso ele falhe, só sabemos que ele falhou, mas não quanto ele avançou no problema. Então vamos começar criando um framework customizado para debug (lib/ch04/dbg.rb):
$dbg_ids = [] def dbg(id, format, *args) if $dbg_ids.include?(id) $stderr.puts(format%args) end end def debug(*ids) $dbg_ids = ($dbg_ids + ids).uniq end def undebug(*ids) $dbg_ids -= ids end def dbg_indent(id, indent, format, *args) if $dbg_ids.include?(id) $stderr.print " "*indent $stderr.puts(format%args) end end
Este código usa uma variável global para decidir quais declarações de debug devem ser usadas. Podemos habilitar ou desabilitar ids específicos usando os métodos debug e undebug. Além disso, podemos usar dbg e dbg_indent para realmente escrever algo na tela.
Perceba que eu usei o operador sprintf (%) para fazer a formatação das strings internas ao sistema.
Agora é hora de olharmos para o GPS2, que apresenta soluções para os problemas “correndo ao redor do quarteirão”, ” derrotas do pré-requisito irmão” (prerequisite clobbers sibling), “saltando antes de olhar” e “subobjetivo recursivo”.
Uma das mudanças mais interessantes é que esta versão não imprimirá nada, mas ao invés disso retornará uma lista de status que descreve as ações tomadas. Mais tarde isto dará mais flexibilidade em relação a como o código é usado.
Antes de começarmos há algumas coisas que precisamos adicionar para fazer com que a saída seja aceitável. Faremos isso representando as operações de forma um pouco diferente, incluindo na lista de adição uma ação composta que incluirá o símbolo executing e o nome da ação. Também precisamos de um equivalente da função some (alguns). Por fim, precisamos ter algo que possa criar novas operações incluindo a parte de execução na lista de adições.
Este código poder ser visto em lib/ch04/gps2.rb:
require 'gps' require 'gps_no_clobber' require 'dbg' module Enumerable def some? self.each do |v| result = yield v return result if result end end end class GPS::Op def convert! unless self.add_list.any? do |l| l.is_a?(Array) && l.first == :executing end self.add_list << [:executing, self.action] end self end end GPS::SCHOOL_OPS.each do |op| op.convert! end class GPS2 < GPS class << self def op(action, parts = {}) GPS::Op.new(action, parts[:preconds], parts[:add_list], parts[:del_list]).convert! end end end
Neste caso eu optei por fazer uma subclasse do GPS2 e usei os helperes existentes no gps e no gps_no_clobber, embora no final das contas a maioria dos métodos serão sobrescritos.
Eu defini o método some? (alguns) no módulo Enumarable; Ele é bastante similar a versão Ruby do find/detect. A única diferença é que precisamos salvar o resultado do bloco e realmente retorná-lo ao invés do valor que enviamos para o bloco.
Abri a classe GPS::Op e adicionei o método convert!. Perceba que nomeei-o com um exclamação (bang) , já que ele pode modificar a instância atual. Também decidi não seguir a convenção retornando nil de um método com exclamação. Ao invés disso, retorno self porque isso fará meu código um pouco mais simples em certas ocasiões. O método convert! checa se a lista de adição já contem um array em que o primeiro elemento é :executing, caso contrário ele adicionará um destes. Isto será mais interessante mais tarde.
O arquivo gps2 também converte todas as operações escolares disponíveis.
Finalmente nós fornecemos um novo método chamado GPS2::op, que irá criar uma nova operação convertida. Perceba que estou usando a convenção de abrir a classe self com class << self. Eu quase sempre prefiro este modo.
O próximo passo é olharmos o novo método solve (resolver). Este será levemente diferente já que agora todos os métodos recebem um argumento de estado para saber em que lugar da avaliação estamos. O único lugar que não recebe o estado é o método solve, já que ele usa o estado inicial dado para inicializar o método. Todos os métodos com exceção do solve também recebem um parâmetro da pilha de objetivos atuais, o que nós ajudará a nos mantermos informado sobre quais objetivos já atingimos.
O método solve se parece com isso (lib/ch04/gps2.rb):
def solve(*goals) achieve_all([[:start]] + @state, goals, []).grep(Array) end
O código original em Lisp usa o átomo remove-if #’ (remove se) para filtrar todo o ruído do estado, mas a versão Ruby é muito mais simplificada pelo simples emprego do grep, enviando-o um Array. Você sabia que o grep pode receber qualquer coisas que implementa o método ===, certo?
Para nos mantermos informado do fato de que começamos, adicionamos a lista [:start] ao ínicio do estado atual.
Existem mais três métodos que precisam ser atualizados para que esta implementação rode. Os dois primeiros são achieve_all e achieve (lib/ch04/gps2.rb):
def achieve_all(state, goals, goal_stack) current_state = goals.inject(state) do |current_state, g| return [] if current_state.empty? achieve(current_state, g, goal_stack) end if goals.subset_of?(current_state) current_state else [] end end def achieve(state, goal, goal_stack) dbg_indent(:gps, goal_stack.length, "Goal: %p", goal) if state.include?(goal) state elsif goal_stack.include?(goal) [] else @ops.find_all do |op| appropriate?(goal, op) end.some? do |op| apply(state, goal, op, goal_stack) end || [] end end
Estes são um pouco mais complicados que na primeira implementação. Isto porque precisamos lidar com algumas coisas com que a primeira implementação não lidava muito bem.
O método achievel_all é definitivamente um pouco mais bagunçado. Isso porque precisamos atualizar o estado atual para cada chamada ao método achieve. No livro de Norvig, a implementação na verdade usa uma variável de estado que era atualizada a cada iteração, mas eu achei que essa modificação explicita não caia bem aqui, especialmente não na presença de inject, que é uma ferramenta perfeita para este tipo de atualização. A única complicação é que nós queremos paralisar tudo se qualquer chamada ao achieve retornar um estado vazio (isso indica que não podemos continuar). Se um objetivo não pode ser alcançado, é óbvio que podemos simplesmente saltá-lo diretamente.
O uso do inject aqui é um pouco diferente já que ele não constrói nada explicitamente mas, ao invés disso, cria uma nova variável que é uma versão modificada da original. Este é um retrabalho (refactoring) que eu gostaria de ver em mutas bases de código, já que é um jeito bastante legal de se livrar da mutação explícita.
Após a construção do estado atual pela chamada de achieve em todos objetivos, também precisamos ter certeza de que todos objetivos ainda fazem parte do estado atual. Fazemos isso com o método subset? (subconjunto) definido no código da primeira versão do GPS.
O método achieve é o primeiro lugar em que usaremos nosso sistema de debugging. O tamanho da pilha de objetivos nos dá um parâmetro óbvio para identação, já que ela ficará maior e maior a medida que a recursão se torna mais profunda. Eu usei %p ao invés de %s pois eu realmente quero a saída inspecionada neste ponto.
O achieve pode rodar de três maneiras. A primeira é quando o objetivo já está no estado atual. Isso significa que já atingimos esse objetivo e tudo está certo.
Se o objetivo está na pilha de objetivos, isso significa que estamos na segunda iteração de uma busca de objetivo recursiva - o que não é bom. Isso significa que falhamos com esse objetivo, portanto retornamos um array vazio.
Finalmente, se nenhuma das situações acima é verdadeira, precisamos encontrar todas as operações apropriadas para o objetivo atual - usando o mesmo método appropriate? do primeiro GPS. Então usamos some? em todas as operações apropriadas. A diferenças entre find/detect e some? fica óbvia aqui - se usassemos find, o resultado seria uma instância de Op, enquanto some? retornará o estado atual retornado pelo método apply chamado dentro do bloco.
No final da avaliação, se some? não encontrar nada, é necessário retornar um array vazio ao invés de nil, já que estou usando o array vazio para representar cada falha (Discutirei isto em maiores detalhes em um artigo comparando Ruby e Common Lisp).
O método faltando no GPS2 é agora o apply. Ele se parecesse com isso (lib/ch04/gps2.rb):
def apply(state, goal, op, goal_stack) dbg_indent(:gps, goal_stack.length, "Consider: %p", op.action) state2 = achieve_all(state, op.preconds, [goal] + goal_stack) unless state2.empty? dbg_indent(:gps, goal_stack.length, "Action: %p", op.action) (state2 - op.del_list) + op.add_list end end
Como você pode ver, usamos dbg_indent em dois lugares diferentes aqui para deixar claro o que está acontecendo enquanto estamos aplicando uma operação. Este também é o lugar em que primeiro fazemos uma recursão para ter certeza de que todas as pré-condições das operações foram satisfeitas. Se recebermos um estado que não está vazio, podemos retornar um novo estado que é modificado pela lista de de remoções e então pela lista de adições.
Uau, isso foi um pouco mais complicado, mas não tão ruim. Vamos ver como essa versão funciona com os problemas que definimos para o GPS anteriormente (lib/ch04/gps2_problem1.rb):
require 'gps2' require 'pp' gps = GPS2.new([:son_at_home, :car_needs_battery, :have_money, :have_phone_book], GPS::SCHOOL_OPS) pp gps.solve(:son_at_school)
Se você executá-lo, verá que o resultado bate mais ou menos com os passos que obtemos no primeiro GPS. Eu usei pp para mostrar o resultado da operação. A principal diferença é que a condição de início esta lá também.
Algo bastante simples, como lib/ch04/gps2_problem2 também funciona bem:
require 'gps2' require 'pp' gps = GPS2.new([:son_at_home, :car_works], GPS::SCHOOL_OPS) pp gps.solve(:son_at_school)
O problema que a última versão costuma errar agora funciona corretamente (lib/ch04/gps2_problem3.rb):
require 'gps2' require 'pp' gps = GPS2.new([:son_at_home, :car_needs_battery, :have_money, :have_phone_book], GPS::SCHOOL_OPS) pp gps.solve(:have_money, :son_at_school)
e lib/ch04/gps2_problem4.rb:
require 'gps2' require 'pp' gps = GPS2.new([:son_at_home, :car_needs_battery, :have_money, :have_phone_book], GPS::SCHOOL_OPS) pp gps.solve(:son_at_school, :have_money)
Olhar antes de saltar também falha como previsto (lib/ch04/gps2_problem5.rb):
require 'gps2' require 'pp' gps = GPS2.new([:son_at_home, :car_needs_battery, :have_money], GPS::SCHOOL_OPS) pp gps.solve(:son_at_school)
Problemas bem simples que não requerem nenhuma ação também funcionam corretamente (lib/ch04/gps2_problem6.rb):
require 'gps2' require 'pp' gps = GPS2.new([:son_at_home], GPS::SCHOOL_OPS) pp gps.solve(:son_at_home)
Tradução - PAIPR - O Resolvedor de problemas gerais (parte 1)
December 27th, 2008
Continuando a série de traduções dos artigos do Ola Bini, traduzirei a seguir este artigo. Posso demorar um pouco, mas pretendo traduzir todos os artigos da série.
O Resolvedor de problemas gerais, Parte 1
Para uma das primeiras tentativas de se criar um sistema de resolução de problemas foi dado o grandioso nome de Resolvedor de Problemas Gerais (General Problem Solver). Foi desenvolvido em 1957 por Alan Newell e Herbert Simon. Quando o RPG foi inicialmente apresentado, este causou bastante agitação na comunidade de IA. Porém o RPG não sobreviveu a suas expectativas, mesmo assim é bastante importante do ponto de vista histórico. A implementação orginal do GPS foi escrita em IPL e tem algumas diferenças sutis em relação ao programa desenvolvido no livro PAIP. Eu seguirei a versão do PAIP enquanto estiver desenvolvendo a versão em Ruby. Se você tem algum interesse na versão original, sugiro que primeiro encontre um livro sobre IPL (que, de maneira alguma, é uma linguagem divertida).
O RPG implementa uma versão de algo chamado análise de meios e fins. O tipo de pensamento abordado por essa análise é de bastante senso comum. Resumidamente, você tem alguns objetivos, você então deve descobrir quais condições necessitam ser verdadeiras para que você atinja um determinado objetivo. Se estas condições não são verdadeiras, você terá que descobrir como atinji-las, e assim por diante.
A maneira que escreveremos a primeira versão do GPS possui algumas propriedades interessantes. As principais são o estado inicial, os objetivos e as operaçõs disponíveis. Por enquanto representarei os estados e objetivos como símbolos. Uma operação precisa ser um pouco mais complicada. Em primeiro lugar, ela possui um nome para que você possa rastreá-la. Ela possui pré-condições que detalham o que precisa ser verdadeiro para que essa operação possa ocorrer. E ela possui uma lista de adições e uma lista de remoções. A lista de adições nos diz quais condições são verdadeiras após a execução da operação e a lista de remoções detalha o que já não é mais verdadeiro.
Então vamos dar uma olhada na primeira versão da implementação. Eu fiz algumas mudanças se comparado ao código Lisp de Norvig. A mais óbvia é que não estou usando variáveis globais. Ao invés delas, opto por colocar tudo em uma classe chamada GPS (este código pode ser encontrado em lib/ch04/gps.rb ).
class GPS Op = Struct.new(:action, :preconds, :add_list, :del_list) def initialize(state, ops) @state = state @ops = ops end # Interface prinicipal do Resolvedor de Problemas Gerais: # atinge todos os objetivos (goals) # usando as operações definidas def solve(*goals) goals.all? do |goal| achieve goal end end # Um objetivo é atingido se ele já foi # atingido ou se há uma operação # apropriada que seja aplicável a ele def achieve(goal) @state.include?(goal) || (@ops.find_all do |op| appropriate?(goal, op) end.any? do |op| apply(op) end) end # Uma operação é apropriada para um # objetivo se ela está na lista de adições def appropriate?(goal, op) op.add_list.include?(goal) end # Imprime uma mensagem e atualiza o estado # caso a operação seja aplicável def apply(op) if op.preconds.all? { |cond| achieve(cond) } puts "Executing #{op.action}" @state -= op.del_list @state += op.add_list return true end false end end
Existem alguns componentes neste código, portanto vamos olhar peça por peça. Em primeiro lugar, criamos uma classe chamada GPS. Dentro dela, imediatamente definimos uma estrutura (Struct) chamada Op. Existe uma simetria entre a operação do Lisp defstruct e a chamada Struct.new do Ruby que muito me aprecia. Neste caso ela é perfeita já que nós não queremos que Op tenha nenhum comportamento neste momento. A Op consiste em uma ação, nas pré-condições, na lista de adições e na lista de remoções.
O método initialize recebe o estado atual e as operações disponíveis (ops). O estado atual é um array de estados e ops é um array de ops.
A peça central neste código é na verdade o método solve (resolver). Este método corresponde a função GPS no código original. A principal diferença é que a função GPS recebe o estado, os objetivos e as ops como parâmetros, enquanto nós colocamos estes dentro do objeto usando o método initialize.
Então o que significa para nós resolver os objetivos? Basicamente nós tentamos atingir todos objetivos. Se pudermos atingir todos objetivos nós conseguimos resolvê-los, caso contrário não conseguimos. O código Lisp de Norvig lida com isso um pouco diferente, retornando o símbolo ‘resolvido’ caso ele funcione. Ao invés disso, eu resolvi apenas retornar um booleano. Isso significa que nosso uso do método solve pode ser um pouco mais verboso.
O método achieve (alcançar) tenta fazer duas coisas diferentes. Em primeiro lugar ele checa para saber se o estado atual já inclui o objetivo. Neste caso estamos bem. Caso contrário primeiro precisaremos encontrar todas as operações que são apropriadas para esse objetivo e então checar se alguma destas operações pode ser aplicada. Antes de olhar para os métodos appropriate? e apply, é interessante notar que eu decidi usar o any? (algum) aqui, enquanto Norvig usa a função chamada some (alguns). Estas duas operaçãos são na verdade um pouquinho diferentes, mas por enquanto essa diferença não é importante. A diferença é que enquanto o any? sempre retornára verdadeiro ou falso, o some retornará o valor não-nulo gerado pelo método test. O Ruby não parecer ter nada parecido com isso (Enumerable#detect/find retorna o valor original, não o valor gerado pelo bloco). Como você verá no próximo artigo (parte 2) eu terei que implementar o some nesse ponto.
O que é o método appropriate? (apropriado) então? Em primeiro lugar, é um método que provavelmente deve pertencer a classe Op no lugar da classe GPS. No momento ele apenas checa se a lista de adições das operações inclui o objetivo - o que significa que tal operação pode ser usada para se alcançar o objetivo.
Finalmente temos o método apply (aplicar, chamado de apply-op no código de Norvig). O Apply é o local onde os elementos recursivos do algoritmo entram em jogo. Basicamente, para que uma operação seja aplicada, todas as pré-condições desta operação precisam ser compridas. Usamos all? para tentar atinger todas a pré-condições e se le é bem sucedido imprimimos quais ações foram executadas, modificamos o estado removendo a lista de remoção e adicionado a lista de adição e, finalmente, retornamos true.
E é realmente isso o que há no resolvedor de problemas gerais. É claro que deveríamos testá-lo e ver, certo? O domínio comum de uso do GPS era o de guiar até a enfermaria e nós vamos usar algo similar a isso para testar esta implementação.
A primeira coisa que precisamos é ter algumas operações definidas para este domínio. Você também pode encontrar esse código em (http://github.com/olabini/paipr/tree/master/lib/ch04/gps.rb?raw=true)[lib/ch04/gps.rb]:
SCHOOL_OPS = [ Op.new(:drivesonto_school, [:sonat_home, :carworks], [:son_at_school], [:son_at_home]), Op.new(:shopinstallsbattery, [:carneedsbattery, :shopknowsproblem, :shophasmoney], [:car_works], []), Op.new(:tellshopproblem, [:incommunicationwith_shop], [:shopknowsproblem], []), Op.new(:telephone_shop, [:knowphonenumber], [:incommunicationwith_shop], []), Op.new(:look_up_number, [:havephonebook], [:knowphonenumber], []), Op.new(:giveshopmoney, [:have_money], [:shophasmoney], [:have_money]) ]
Como você pode ver, este domínio dá uma dica sobre o que pode entrar em jogo nos objetivos do domínio. Então, vamos começar dando uma olhada nos problemas em questão. O primeiro problema está em [lib/ch04/problem1.rb]():
require 'gps' gps = GPS.new([:son_at_home, :car_needs_battery, :have_money, :have_phone_book], GPS::SCHOOL_OPS) if gps.solve(:son_at_school) puts "Solved" else puts "Not solved" end
O estado atual diz que o filho está em casa, o carro precisa de uma bateria e que nós temos dinheiro e uma lista telefônica. O objetivo a se alcançar é levar o filho até a escola. Se executarmos este código ele gera a seguinte saída:
Executing look_up_number Executing telephone_shop Executing tell_shop_problem Executing give_shop_money Executing shop_installs_battery Executing drive_son_to_school Solved
Bem, isso parece ter funcionado corretamente, certo? E no caso em que não temos nenhuma lista telefônica? (lib/ch04/problem2.rb):
require 'gps' gps = GPS.new([:son_at_home, :car_needs_battery, :have_money], GPS::SCHOOL_OPS) if gps.solve(:son_at_school) puts "Solved" else puts "Not solved" end Isso gera: <filter:code lang='ruby'> Not solved
como esperávamos. E se pegarmos um exemplo bem simples em que o carro já funciona (lib/ch04/problem3.rb):
require 'gps' gps = GPS.new([:son_at_home, :car_works], GPS::SCHOOL_OPS) if gps.solve(:son_at_school) puts "Solved" else puts "Not solved" end
Obtemos:
Executing drive_son_to_school Solved
Legal. Parece bom. Então quais são os problemas com a abordagem atual? Em primeirlo lugar, ela não resolve alguns problemas bem fáceis. A representação é em geral incorreta para atividades contínuas. Norvig chama esse problema de “correndo ao redor do quarteirão”. É fácil definir um objetivo de dirigir de casa para a escola, mas é um pouco mais complicado representar uma corrida ao redor do quarteirão. Existem modos de fazê-lo, claro, mas o operador GPS não os tornam necessariamente naturais como poderiam ser.
Outro problema mais sério é o problema do objetivo do irmão derrotado. No lib/ch04/problem4.rb a versão do GPS lida com tudo corretamente:
require 'gps' gps = GPS.new([:son_at_home, :have_money, :car_works], GPS::SCHOOL_OPS) if gps.solve(:have_money, :son_at_school) puts "Solved" else puts "Not solved" end
Mas se criarmos um problema como este (lib/ch04/problem5.rb):
require 'gps' gps = GPS.new([:son_at_home, :car_needs_battery, :have_phone_book, :have_money], GPS::SCHOOL_OPS) if gps.solve(:have_money, :son_at_school) puts "Solved" else puts "Not solved" end
Isto será incorretamente reportado como resolvido - já que o GPS resolve primeiro o problema de termos dinheiro, então resolve o problema de termos o filho na escola. O modo de lidarmos com esse problema é substuirmos cada instância que atinge cada chamada com uma chamada para um novo método chamado achieveall (alcança tudo). Isso envolve os métodos solve e apply. você pode ver este código em lib/ch04/gpsno_clobber.rb:
require 'gps' class Array def subset_of?(other) (self - other) == [] end end class GPS def solve(*goals) achieve_all goals end def apply(op) if achieve_all(op.preconds) puts "Executing #{op.action}" @state -= op.del_list @state += op.add_list return true end false end def achieve_all(goals) goals.all? do |goal| achieve goal end && goals.subset_of?(@state) end end
Aqui eu apenas decidi abrir a classe GPS para prover esta nova funcionalidade. Outra opção seria criar uma subclasse, mas já que isso substancialmente não altera a funcionalidade, decici apenas fazer deste modo. Existem algumas coisas acontecendo neste código. Primeiro decidi adicionar um operador a classe Array, que se espelha na função susetp do Common Lisp. Array.subsetof? retorna verdadeiro se o array atual é um subconjunto do array de argumentos, então [].subsetof?([1]) == true, enquanto [1].subsetof?([]) == false. O método solve é atualizado para apenas chamar o achieveall, e apply também chama achievaall. A definição do método achieveall primeiro tem certeza de que podemos atingir todos objetivos e então tem certeza de que todos outros objetivos a se atingir ainda estão no estado atual.
Se você executar lib/ch04/problem6.rb:
require 'gps_no_clobber' gps = GPS.new([:son_at_home, :car_needs_battery, :have_phone_book, :have_money], GPS::SCHOOL_OPS) if gps.solve(:have_money, :son_at_school) puts "Solved" else puts "Not solved" end
Verá que ele executa algumas ações mas então retorna um não resolvido, já que não é possível atingir ambos objetivos.
Outro problema com esta implementação é baseado na intercalação entre planejamento e execução. Norvig chama isso de “o problema de saltar antes de olhar”. Você pode ver um exemplo deste problema na última execução - isto é, vemos que fizemos várias coisas, mas então percebemos que não podemos resolver o problema. No final da execução nos já consertamos o carro dando o dinheiro a oficina. Isso pode não ter sido muito bom. Já que nós temos apenas uma variável de estado, e isto já foi alterado, não há como voltarmos atrás.
Há um problema final, com subobjetivos recursivos. Isto é, esta versão do GPS não é tão boa em lidar com um caso em que o objetivo depende de outro objetivo que depende do primeiro. O arquivo lib/ch04/problem7.rb exemplifica isso:
require 'gps' gps = GPS.new([:son_at_home, :car_needs_battery, :have_money], GPS::SCHOOL_OPS + [ GPS::Op.new(:ask_phone_number, [:in_communication_with_shop], [:know_phone_number], []) ]) if gps.solve(:son_at_school) puts "Solved" else puts "Not solved" end
Nós removemos o estado :havephonebook do estado inicial e adicionamos uma nova operação para pedir pelo número de telefone. Esta operação requer que você entre em contato com a oficina e, já que você precisa do número de telefone para isso, isso resultará num SystemStackError (erro de pilha do sistema) no Ruby.
Uma última perturbação com a implementação atual é que ela apenas diz verdadeiro ou falso. A única informação que recebemos é a saída impressa na tela.
Tudo isso são coisas que a versão final do GPS irá lidar mais corretamente - pelo menos em alguns casos. Este artigo já está sufcientemente longo, então o GPS2 terá que esperar para o próximo!
Deixando seu código mais legível
December 6th, 2008
Finalmente saiu a maioria dos vídeos da Rubyconf 2008. E tem muito material interessante. Como prometido, falarei um pouco da palestra de Micheal Granger. Achei a palestra razoável, algumas coisas um pouco óbvias e algumas dicas que eu não tinha conhecimento.
Apresentarei aqui, resumidamente, as idéias passadas pelo palestrante.
Você encontra Michael Granger no canal ruby-talk da freenode, seu apelido é ged. Ele utiliza Ruby desde 2001 e esteve em 6 Rubyconfs desde então. Granger trabalha em um estúdio de animação em Portland chamado Laika. Ele participa ativamente de uma dezena de projetos open source como, por exemplo, o Bluecloth e criou o tema padrão do RDoc.
A idéia de Granger é a de aplicar os príncipios do design visual para o código com objetivo de melhorar a comunicação deste com os seus leitores . O palestrante falou que evitaria entrar em questões polêmicas como, por exemplo, se é melhor utilizar dois ou quatro espaços para identar o código. Estes tipos de detalhes são escolhas pessoais que variam com o estilo do programador. O que ele apresentará são sugestões que nem sempre servirão para você mas em geral são válidas.
Código é comunicação. É um conjunto de instruções para uma máquina. Mas é primariamente uma comunicação de idéias entre seres humanos. Estas pessoas, a audiência do código, são seus parceiros de trabalho, outras pessoas que irão alterar o código (no caso de um projeto open source) ou aprender com ele e, em muitos casos, você mesmo, que terá que olhar para um código seu escrito a meses atrás para melhorá-lo, consertá-lo ou adicionar funcionalidades. É normal olharmos para o nosso próprio código e pensarmos, “Puxa, eu escrevi isso!?”
Pa lermos o código de maneira eficiente precisamos maximizar o “fluxo”. O Fluxo, segundo Mihaly Csikszent, é “a sensação de completude e foco energizante em uma atividade, com alto nível de prazer e satizfação”. Existem alguns critérios para que o fluxo seja mantido:
- imersão completa no que faz;
- sabe o que tem que fazer a cada momento;
- tem um feedback rápido e preciso de quão bem você está fazendo;
- e um sentimento de que suas habilidades são desenvolvidas mas não são superadas pelas necessidades de ação.
Com pouco esforço podemos fazer mais para tornar nosso código mais legível e assim aumentar o fluxo. Por que não gostariámos de fazê-lo?
Por misantropia, exagerando, por não gostarmos da humanidade. Alguns progamadores acreditam que ler código deve ser difícil, que nem todas as pessoas têm o direito de entender o que se passa. Ou por pânico em querer terminar o código o mais rápido possível. Porém a comunidade Ruby em geral não sofre destes dois problemas. O problema principal é a austeridade, isto é, um senso de simplicidade na aparência e estilo do código, já que a linguagem nos incentiva a escrever as coisas de maneira simplificada. Mas nem sempre devemos tomar este caminho.
Então, o design adiciona estruturas e signficados para a comunicação usual. Granger pergunta: Sou um programador, o que eu sei sobre design visual? Ele nos introduz ao livro: “The non-designers design book” de Robin Williams, uma boa introdução para programadores sobre design que foi recomendado ao palestrante por um colega da área de usabilidade.
Este livro apresenta os quatro príncipios básicos do design (a sigla CRAP):
Contraste
Coisas que são diferentes devem parecer bastante diferentes. O mais importante numa página deve ser aquilo que chama a atenção do usuário na primeira vez em que ele olha para ela.
Por convenção, já aplicamos várias regras para constraste:
# Símbolos que são adicionados a variáveis ditinguem # entre identificadores comuns e identficadores não usuais): variavel_local @variaveis_de_instancia @@variaveis_de_classe $globais # Constantes que na verdade são classes contra constantes "Puras" BasickSocket DEFAULT_DEPTH # Blocos de uma linha contra blocos de multiplas linhas ary.map {|thing| thing.inspect } ary.each do |thing| next if thing.tainted? || thing.imediate_object? list << thing end
Granger então dá algumas sugestões.
Distinguirmos chamadas a métodos de variáveis locais.
filters.each{ |filter| ... }
Neste código, filters é uma variável local ou uma chamada de método? A distinção entre estes dois tipos pode ser facilmente obtida adicionando um ”self.” no caso em que o parâmetro em questão é um método, mostrando que há uma chamada de método da classe em questão. O ponto indica que há uma mensagem sendo passada, portanto uma ação.
self.filters.each{ |filter| ... }
Olhando para o mesmo código não há ambiguidade. Não precisaremos procurar no código para saber o que é “filters” pois já sabemos imediatamente que é uma chamada de método.
Outro exemplo são métodos de classe. Olhando para os métodos abaixo (parte do código do ActionController::Base do Rails) é difícil saber que eles são métodos de classe apenas olhando para esta parte do código.
def prepend_with_path( path ) ... ... ... end def append_view_path( path ) ... ... ... end
Isto porque esta informação está 300 linhas acima:
class << self
Então você pode usar o estilo de declaração de método do singleton, empregando o operador de escopo (*::*) e deixando mais claro que estes são métodos de classe e não métodos de instância nem apenas mensagens sendo passadas.
def self::prepend_with_path( path ) ... ... ... end def self::append_view_path( path ) ... ... ... end
Repetição
Coisas similares devem permanecer parecidas. A consistência é um ponto chave.
No código podemos alterar as relações espaciais e as espessuras das linhas.
A primeira sugestão dada é a de se usar comentários para o auxílio na navegação.
##################################### ### C L A S S M E T H O D S ##################################### ##################################### ### I N S T A N C E M E T H O D S #####################################
O uso destes blocos é interessante para marcar diferentes escopos dentro do código.
Um segundo marcador menos óbvio e forte no código é usar marcadores para destacar modificadores de comportamento do código.
############## funcao_de_modulo ############## ##### public ##### ####### protected ####### ##### private #####
E o terceiro método é usar marcadores para delinear métodos. Assim, os códigos no alto dos métodos serão distinguidos do código dentro do método no Rdoc.
### Method documentation def a_method # comments of a trick bit of the method self.some_trucky_stuff end
Alinhamento
Coisas que tem alinhamento em seu propósito devem estar alinhadas visualmente. Os elementos não podem ser dispostos arbitrariamente.
A maioria das pessoas já segue esse conselho em algum nível. É comum que usemos identação de código. Existem outras coisas que você pode fazer, além de identar o código. Como, por exemplo, alinhar os operadores, para que fique mais fácil de se ler que um grupo de linhas está fazendo coisas similares (no exemplo aqui no blog não ficou perfeito já que essa fonte não é mono-espaçada).
def initialize( name, description, host, port ) @name = name @description = description @host = host @port = port end
E também alinhar cláusulas condicionais no final de uma linha:
seld.send_notification if self.something_has_changed && self.user_wants_notification? && self.user_is_reachable?
Ou métodos indivduais de uma longa cadeia de métodos:
lines.collect {|str| str.chomp }. reject {|str| str =~/^\s*($.+)$/ }. collect {|str| str.scan(/\w+/) }. flatten. uniq
Outra boa prática é adicionar espaço aos seus parênteses (colchetes e chaves para o casso de arrays e hases) para tornar mais fácil a visualização dos argumentos.
def initialize( name, description ) ... end servers = [ ['localhost', 1414], ['staging', 2014] ]
Proximidade
Coisas relacionadas devem ser agrupadas.
Michael acredita que esse é o mais importante dos príncipios. Ele diz que um exemplo básico disso é a organização de seus arquivos. É interessante também o uso de espaçamento vertical (linhas em branco) para separar partes do código que se relacionam para que esta relação fique clara visualmente. Métodos como vários argumentos não devem ter os parênteses omitidos, isso dificulta a letura.
Concluindo, Granger nos reitera que código é comunicação e que devemos aprender com os designers para que possamos escrever código mais legível e assim economizar tempo no futuro, quando nós mesmos ou outras pessoas lerão nosso código.
Depois dessa palesta, já vi que tenho muitas linhas de código a serem editadas. Se tiverem sugestões de como escrever um código mais legível, não deixem de comentar.
O que torna um código belo?
November 7th, 2008

Nesta semana está acontecendo a Rubyconf 2008 e uma das palestras que me chamou a atenção é a de Michael Granger que tratará sobre como deixar seu código Ruby mais legível, comunicativo e mais fácil de se manter. Para tal ele irá aplicar o príncipio de design visual de Robin Williams conhecido como CRAP (sigla para Contraste, Repetição, Alinhamento e Proximidade). Para quem não entende inglês, o significado da palava “crap” é lixo, porcaria. Além disso ele usará algumas idéias do método de programação espartano (Spartan Programming), Michael Schwern, Marcel Molina Jr. e do grande mestre dos computeiros, Donald Knuth.
Pois, justamente, semana passada assisti uma das palestras da Rubyconf 2007 no site da Confreaks (espero que as palestras da Rubyconf 2008 também estejam lá). Era a palestra de Marcel Molina Jr. e ele falava sobre o que faz um código belo. Esta palestra também foi uma keynote na Ruby Hoedown 2007. Tentarei apresentar aqui sua essência.
Marcel disse que não faria uma apresentação acadêmica. Afirmou que, ao pensamos em beleza, geralmente pensamos em aparência e nos vem a imagem de um bebê, uma mulher ou uma flor. Mas essa definição é muito superficial.
Software belo é a mesma coisa que um bom software. Como já dizia o pensador francês Jean-Jacques Rousseau. “Bom nada mais é que a beleza em ação e ambos tem uma fonte comum na natureza organizada”.
O conceito de natureza organizada é explicado por Pitágoras, que conhecemos no segundo grau, nas aulas de geometria. Ele dizia que a beleza vem dos números, eles estão em toda a parte. “Todas as coisas existem por serem ordenadas. E elas são ordenadas porque são a realização de leis matemáticas”. Pitágoras ouviu a beleza de som que vinha dos martelos de um ferreiro, uma verdadeira melodia. Então observou que existia uma proporção entre os diferentes tamanhos de martelo. Este conceito de proporcionalidade também foi usado nas construções arquitetônicas da Grécia antiga.
Contudo foi a definição de São Tomas de Aquino aquela que o palestrante explorou com maior profundidade e que ele acredita que melhor se encaixa com software. Para o sábio teólogo a beleza é definida por três regras:
a proporção, isto é, apresentar uma boa estrutura em que a relação de tamanho entre as diferentes partes do objeto seja proporcional, mantendo o tamanho o menor possível.
a integridade, isto é, o objeto deve servir para o propósito em questão. Ele exemplifica falando do martelo de cristal: tem beleza física, mas não tem integridade pois não é uma boa solução para o problema de bater pregos, já que se quebraria em diversos partes se fosse usado para tal propósito.
a clareza, isto é, o objeto deve ser simples/claro. Vide os designs desenvolvidos pela Apple.
Em seguida, Marcel dá um exemplo para mostrar como estes três conceitos se aplicam a código. O programa usado como exemplo deve converter strings de um arquivo XML para tipos correspondentes (fazer a coerção) em Ruby:
"true" => true "false" => false "42" => 42
Ele então chegou a seguinte implementação:
class CoercibleString < String attr_accessor :generator def coerce attempt = nil break unless (attemp = coercions.next).nil? while coercions.next? attempt.nil? ? self : attempt end private def coercions Generator.new do |self.generator| try { self == 'true' } try { [self == 'false', false]} try { Integer(self) } try { Date.parse(self) } end end def try attempt, desired = yield generator.yield(desired.nil? ? attempt : desired) if attempt rescue ArgumentError generator.yield nil end end end
Ele mostra também a implementação final do programa:
Class String def self.coerce(string) case string when 'true' : true when 'false' : false when /^[1-9]+\d*$/ : Integer(string) when DATETIME_FORMAT: Time.parse(string) else string end end end
Marcel explica brevemente o funcionamento da solução e então avalia a beleza da primeira implementação.
Proporcionalidade: em relação as partes do código (métodos) são mais ou menos proporcionais. Porém, se formos olhar a quantidade de código, é realmente necessário tanto código para resolver o problema? A segunda solução tem metade do tamanho, portanto a proporcionalidade da primeira solução é bem ruim.
Integridade: No Ruby 1.9 os generators (usados pela primeira solução) são implementados com threads, mas no Ruby 1.8 eles usam continuations, o que é bastante lento. Mudando para a segunda implementação os testes de Marcel rodaram uma ordem de magnitude mais rápidos em relação a primeira. Ou seja, falhamos também na integridade.
Claridade: Marcel diz que teve que explicar o funcionamento do primeiro código e diz saber que ele não é claro. Já o segundo é bem mais claro (alguém tem dúvidas disso?).
Inicialmente, ele não sabia que o código era ruim. Marcel pensou que poderia ser um jeito novo, interessante de se resolver o problema de coerção. Mas, no final das contas, mostrou-se não ser um belo código.
Então ele afirma: Você não escolhe um ou dois princípios. Os três são necessários e nenhum deles é suficiente.
Por exemplo, não se deve sacrificar a clareza em prol da proporcionalidade, isto é, criar códigos curtos que são incompreensíveis.
Você pode me explicar o que fazem os códigos a seguir? Bem, deixa pra lá.
expand(join("",(map { /\s+\z/ ? ( $_ ) : ($_, ' ') } @t), $tail)); foreach (@ARGV) { /(\.[^.]+)$/ && $exten{$1}++ foreach glob "$_/*.*" }
O palestrante então nos introduz ao livro Smalltalk Best Practices Patterns de Kent Beck (sim, o mesmo cara por trás do XP). Neste livro dos anos 80, o autor trabalha com a linguagem Smalltalk e dá algumas dicas de como desenvolver software de melhor qualidade;
Ele então cita algumas frases do livro:
- “Este padrão enfatiza a legibilidade.”
- “Métodos de uma linha estão lá para comunicar-se.”
- “Objetos são mais fáceis de se gerenciar se são divididos em pequenas partes.”
- “A maioria dos bons métodos escritos em Smalltalk cabem em umas poucas linhas.”
- “Você não gasta três ou quatro linhas para expressar iteração, você gasta uma palavra.”
- “O problema de um código como este é que você não pode lê-lo e entender o que se passa”.
E estas frases nada mais fazem que afirmar os três conceitos de beleza de São Tomás de Aquino.
Marcel nos pergunta, como tudo isso dito aqui é útil?
Bem, o código da segunda implementação que taxamos como belo não tem nada de especial. Mas, a 40 anos atrás isso era fantástico, inovador. Imagine trabalhar com assembly e ter a funcionalidade do switch (case). Mas agora, Ruby é o jeito mais belo de se escrever software nos últimos 20 anos embora ele pareça simples. É um alvo em movimento.
John McCarthy, o criador da linguagem Lisp (ele apenas especificou a linguagem, não a implementou) introduziu o conceito de if_” na programação. Você consegue imaginar programação sem o _if? As pessoas trabalhavam sem ele. Mas, quando introduzido, o _if_ deve ter sido um belo conceito.
E aqui vai uma frase interessante de Niels Bohr. “Um especialista é uma pessoa que cometeu todos erros que poderia ter feito num campo bem restrito”.
Assim Marcel fala que sua implementação inicial de coerção foi um erro. Assim como, quem inicialmente introduziu o switch poderia ter optado por uma implementação padrão na época.
Mas ele tentava fazer algo diferente que fosse melhor. E no final, em relação a beleza, o código era ruim.
Ruby é otimizada para beleza. O que Marcel tenta chegar com isso é, que quando trabalhando com software:
- tente imaginar melhores modos de expressão
- violações das três regras da beleza revelam erros - se você perceber que está violando as regras tente fazer ajustes ou reescreva o código
- se você fizer isto suficientemente você pode terminar por obter algo inovador e belo
Esta é uma simples abordagem que Marcel tem usado e que ele acredita que tem sido um sucesso devido ao bom retorno que tem recebido. O palestrante finaliza agradecendo ao Matz e ao Ruby Core Team por terem criado Ruby, que, na sua opinião, é a linguagem de programação mais bela em uso.
Espero que este texto tenha sido útil, apesar de eu ter violado as regras de beleza!
Assim que eu tiver alguma informação da palestra de Michael Granger na Rubyconf 2008, tratarei sobre ela aqui.
Tradução: Paradigmas da Programação para Inteligência Artificial - introdução e parte 1
October 10th, 2008
Este artigo é uma tradução do artigo original de Ola Bini. Agradeço ao autor por permitir que eu realizasse tradução de seus artigos desta série. Sintan-se livres para apontar erros e sugestões para melhorias na tradução.
Acho que se você se interessa por Ruby, deve saber quem é Ola Bini. Se não conhece, recomendo que leia a entrevista feita pelo Akita.
Eu tenho estudado algumas coisas ligadas a IA (Inteligência Artificial), especialmente aprendizado de máquina (Machine Learning - ML). Pretendo escrever aqui alguns artigos de ML no futuro. Enquanto isso vou começar com esta tradução, já que achei a série de artigos do Ola bastante interessante. O código dele também é muito bem escrito.
Paradigmas da Programação para Inteligência Artificial (em Ruby)
Já que eu não me canso de participar de diferentes projetos, decidi inciar algo em que pensei a começar faz um bom tempo. Existem algumas razões para fazê-lo, a principal entre elas é que eu queria voltar a brincar com IA, e eu queria ter um projeto com muitas peçinhas que eu possa construir quando tenho algum tempo livre. Se, ao mesmo tempo, o projeto acabar por se tornar educacional ou útil para outras pessoas, bem, não estou reclamando!
Então, o quê é?
Bem, primeiro eu gostaria de apresentar um livro. Ele se chama Paradigms of Artificial Intelligence Programming ou, de maneira mais curta, PAIP. Foi escrito por Peter Norvig, que também escrevel alguns outros livros sobre IA. Atualmente ele é diretor de pesquisa no Google. O PAIP é provalmente meu livro preferido de IA, e também meu livro favorito de Common Lisp. É realmente um livro excelente. De verdade. Se você tem algum interesse em um destes dois assuntos você deve adquirí-lo. O PAIP não cobre assuntos de ponta da IA. Ao invés disso ele usa a visão histórica e analisa diversos exemplos de épocas diferentes, indo dos primeiros programas até coisas bastante avançadas.
Eu o li inúmeras vezes, examinei o código, refinei-o e assim por diante. É muito divertido. Mas isso foi faz alguns anos. Então, basicamente, o que quero fazer é explorar o livro de novo. Mas desta vez escreverei todos os programas em Ruby - convertendo-os do Common Lisp e então talvez melhorando-os um pouco para fazê-los mais idiomáticos. E planejo colocar aqui. Ou melhor, vou colocar o código fonte em http://www.github.com/olabini/paipr. Irei blogar sobre o código que escrever. Você não tem necessariamente que possuir o livre, já que eu irei cercar o código com algumas descrições e explicações.
Mais uma vez, então, por que alguém deveria se importar? Bem, não sei. Talvez ninguém ligue. Mas para mim, pessoalmente, será uma experiência interessante converter Common Lisp idiomático para Ruby idiomático. Será divertido revisitar as antigas abordagens da IA. E poderá servir como uma boa introdução, com bastante código, ao assunto para qualquer um que se interesse por ele.
Eu tenho a permissão de Peter Norvig para fazer isto. O código Ruby que estou escrevendo é coberto pela licença MIT, enquanto qualquer código Lisp colocada como parte deste exercício é coberto por esta licença: http://norvig.com/license.html.
Também atente-se ao fato que nem sempre escreverei o código Ruby mais óbvio - Será bom que ele tenha algumas conexões com código Lisp original.
Parte 1 - Geração de Linguagem
Este artigo é o primeiro da série de artigos sobre o PAIPr. Leia a introdução acima para entender mais sobre o conceito.
Hoje eu gostaria de começar olhando o Capítulo 2. Você pode encontrar o código em lib/ch02 no repositório.
O Capítulo 2 introduz o Common Lisp através da criação de uma séria de maneiras de se fazer a geração de sentenças em inglês, baseando-se em simples gramáticas. É um capítulo interessante para se começar, visto que o código é simples e então é simples comparar as versões em Ruby e Common Lisp.
A primeira parte que temos que considerar é o arquivo common.rb, que contém dois métodos que precisaremos mais tarde:
require 'pp' def one_of(set) [set.random_elt] end class Array def random_elt self[rand(self.length)] end end
Como você pode ver eu também requeri o pp, para facilitar a a impressão de estruturas posteriormente.
Ambos os métodos oneof (um de) e e randomelt (elemento aleatório) são métodos extremamente simples, mas é sempre legal ter este tipo de abstração. Estou mantendo a mesma nomenclatura do livro para estes dois métodos.
require 'common' def sentence; noun_phrase + verb_phrase; end def noun_phrase; article + noun; end def verb_phrase; verb + noun_phrase; end def article; one_of %w(the a); end def noun; one_of %w(man ball woman table); end def verb; one_of %w(hit took saw liked); end
Como você pode ver, todos os métodos apenas definem sua estrutura combinando o resultado de métodos mais básicos. Uma frase substantiva (noun_phrase) é formada por um artigo (article) e então um substantivo (noun). Um artigo é ‘o/a’ (the) ou ‘um/uma’ (a) e um substantivo pode ser ‘homem’ (man). ‘bola’ (ball), ‘mulher’ (women) ou ‘mesa’ (table). Se você executar a sentença algumas vezes verá que em algumas ocasiões irá obter sentenças perfeitamente plausíveis como [“uma” “bola”,”acertou”,”a”,”mesa” ]. Mas você também pode obter coisas menos interessantes, como [“uma”,”bola”,”acertou”,”uma”,”bola”]. Neste ponto o espaço para variação é bastante limitado, mesmo assim você pode notar que há uma versão simplificada da língua inglesa nestes métodos.
Para criar um exemplo que envolve algumas estruturas mais interessantes, podemos introduzir adjetivos (adjectives) e preposições (prepositions). Dado que estes podem se repetir zero ou muitas vezes, nós iremos usar uma produção chamada PP* e Adj* (ppstar e adjstar no código). Isto está no simple2.rb:
require 'simple' def adj_star return [] if rand(2) == 0 adj + adj_star end def pp_star return [] if rand(2) == 0 pp + pp_star end def noun_phrase; article + adj_star + noun + pp_star; end def pp; prep + noun_phrase; end def adj; one_of %w(big little blue green adiabatic); end def prep; one_of %w(to in by with on); end
Nada muda muito aqui, exceto que em ambas produções opcionais retornamos aleatoriamente um array vazio em 50% do tempo. E então eles podem se chamar recursivamente. A produção de frases substantivas também muda um pouco, e adj e prep nos dão os dois novos terminais necessários. Se você tentar usar deste modo, poderá obter alguns resultados ainda mais interessantes, como por exemplo: [“uma”, “mesa”, “pegou”, “um”, “grande”, “adiabático”, “homem”]. É claro que ainda permanece sem sentido. E parece que essa abordagem com aleatoriedade gerará algumas saídas bem grandes em alguns casos. Para fazer esta solução realmente boa é provável que tenhamos que incluir algum viés redutor nos adjetivos e preposições baseado no tamanho da string já gerada.
Outro problema com esta abordagem é que ela é meio incômoda. Usar métodos para a gramática provavelmente não é uma boa escolha a longo prazo. Mais especificamente, nós ficamos atados a esta implementação tendo a gramática sendo representada por métodos.
Uma alternativa viável é representar tudo como uma definição de gramática - usando uma solução baseada em regras. A primeira parte do arquivo rule_based.rb se parece com isto:
require 'common' # Uma gramática para um subconjunto trivial da língua inglesa $simple_grammar = { :sentence => [[:noun_phrase, :verb_phrase]], :noun_phrase => [[:Article, :Noun]], :verb_phrase => [[:Verb, :noun_phrase]], :Article => %w(the a), :Noun => %w(man ball woman table), :Verb => %w(hit took saw liked)} # A gramática usada pelo gerador. Inicialmente ela é $simple_grammar, mas # podemos mudar para outras gramáticas $grammar = $simple_grammar
Note que estou usando arrays duplos para as produções que não são terminais. Existe uma razão para isso que ficará mais clara mais tarde com as gramáticas que se baseiam nisto. Neste instante, entretanto, é fácil ver que uma produção é ou uma lista de palavras, ou uma lista de lista de produções. Os nomes das produções que começam com maiúsculas são terminais - esta é uma convenção na maioria das gramáticas. Eu não utilizei letras maiúsculas quando usando os métodos pois os métodos em Ruby nomeados desta maneira podem causar problemas adicionais quando chamados.
Agora que realmente temos a gramática, necessitamos também de um método ajudante (helper), PAIP define rule-lhs, rule-rhs e rewrites (reescreve), mas o único que realmente precisamos aqui é o rewrites (Do arquivo rule_based.rb):
def rewrites(category) $grammar[category] end
E, na verdade, poderíamos nos virar sem ele também, mas com ele a legibilidade fica melhor em relação a usar um indexador de acesso.
A última coisa que precisamos é o método que realmente cria uma sentença a partir da gramática. Ele se parece com isso:
def generate(phrase) case phrase when Array phrase.inject([]) { |sum, elt| sum + generate(elt) } when Symbol generate(rewrites(phrase).random_elt) else [phrase] end end
Se o que nos é perguntando para gerar é um array, então geraremos tudo o que está dentro deste array e então combinamos estes elementos. Se a produção for um símbolo, então pegamos todas possíveis reescritas e pegamos um elemento aleatório delas. Atualmente toda produção tem uma reescrita, então o ramdom_elt não é estritamente necessário - mas como você verá mais tarde ele é bem bacana. E finalmente, se frase não for um array nem um símbolo, nós apenas retornaremos a frase como o elemento gerado.
Eu gosto especialmente do uso do método inject como uma versão mais geral de (mappend #gera a frase). Claro que, por legibilidade, seria possível também implementar o método mappend:
def mappend(sym, list) list.inject([]) do |sum, elt| sum + self.send(sym, elt) end end
Mas ao invés disso eu preferi utilizar inject diretamente, já que ele é mais idiomático. Note que esta versão do mappend não funciona exatamente igual ao mappend do Common Lisp, visto que ela não permite uma função lambda.
Voltando ao método generate (gera). Se você rodasse generate(:sentence), você teria o mesmo tipo de saída do que rodando com a versão baseada no método - com a diferença de que mudar as regras é muito mais simples agora.
Então, por exemplo, você pode usar esse código da bigger_grammar.rb , que cria uma definição grande de gramática e então usá-la como gramática padrão:
$bigger_grammar = { :sentence => [[:noun_phrase, :verb_phrase]], :noun_phrase => [[:Article, :'Adj*', :Noun, :'PP*'], [:Name], [:Pronoun]], :verb_phrase => [[:Verb, :noun_phrase, :'PP*']], :'PP*' => [[], [:PP, :'PP*']], :'Adj*' => [[], [:Adj, :'Adj*']], :PP => [[:Prep, :noun_phrase]], :Prep => %w(to in by with on), :Adj => %w(big little blue green adiabatic), :Article => %w(the a), :Name => %w(Pat Kim Lee Terry Robin), :Noun => %w(man ball woman table), :Verb => %w(hit took saw liked), :Pronoun => %w(he she it these those that)} $grammar = $bigger_grammar
Esta gramática inclui alguns elementos que fazem a saída um pouco melhor. Por exemplo, nós temos nomes (names) aqui, e também pronomes (pronouns). Uma das razões para esta gramática ser mais fácil de usar é porque podemos definir diferentes versões das produções. Então uma frase substantiva pode, por exemplo, o mesmo que definimos antes, mas também pode ser apenas um único nome ou um único pronome. Nós usamos isso para lidar com as produções recursivas PP* e Adj*. Você pode também ver porque as produções são definidas com um array dentro de um array. Isto é para permitir opções nesta gramática.
Uma sentença típica desta gramática (chamando generate(:sentence)) resulta em [“Terry”, “saw”, “that”] (Terry viu aquilo) ou [“Lee”, “took”, “the”, “blue”, “big”, “woman”] (Lee pegou a grande mulher azul”).
Portanto é mais fácil mudar estas regras. Além disso, acredite que é mais fácil ler e entender as regras aqui. Mas uma das mudanças mais importantes trazidas pela abordagem orientada a dados é que você pode usar as mesmas regras para diferentes propósitos. Diga que queremos uma árvore de sentenças, que inclui o nome da produção usada para esta parte na árvore. Isto é tão simples quanto definir um novo método generate (no arquivo generate_tree.rb):
require 'bigger_grammar' def generate_tree(phrase) case phrase when Array phrase.map { |elt| generate_tree(elt) } when Symbol [phrase] + generate_tree(rewrites(phrase).random_elt) else [phrase] end end
Esse código segue os mesmos padrões que o generate, com algumas pequenas mudanças. Você pode ver que no lugar de anexar os resutados do array em conjunto, nó apenas usamos o método map em cada elemento, Isto porque precisamos de mais subarrays para criar uma árvore. Da mesma maneira quando temos um símbolo nós prefixiamos isto ao array gerado. E, realmento, nesta altura é bem interessante darmos uma olhada na versão Lisp deste código:
(defun generate-tree (phrase) (cond ((listp phrase) (mapcar #'generate-tree phrase)) ((rewrites phrase) (cons phrase (generate-tree (random-elt (rewrites phrase))))) t (list phrase)))
E você pode ver que a estrutura é, na maior parte, a mesma. Eu fiz algumas escolhas diferentes na representação, o que significa que estou checando se a frase é um símbolo ao vés de ver se a reescrita de um símbolo é não nula. A chamada a mapcar é equivalente a chamada de map em Ruby.
O que ele gera então? Chamando-o como “pp generate_tree(:sentence)”, obtenho algo como isto:
[:sentence, [:noun_phrase, [:Name, "Lee"]], [:verb_phrase, [:Verb, "saw"], [:noun_phrase, [:Article, "the"], [:"Adj*", [:Adj, "green"], [:"Adj*"]], [:Noun, "table"], [:"PP*"]], [:"PP*"]]]
que mapeia muito bem para nossa gramática. Nós podemos gerar todas possíveis sentenças para uma gramática sem recursão, usando a mesma abordagem orientada a dados.
O código para esta abordagem é encontrado em generate_all.rb:
def generate_all(phrase) case phrase when [] [[]] when Array combine_all(generate_all(phrase[0]), generate_all(phrase[1..-1])) when Symbol rewrites(phrase).inject([]) { |sum, elt| sum + generate_all(elt) } else [[phrase]] end end def combine_all(xlist, ylist) ylist.inject([]) do |sum, y| sum + xlist.map { |x| x+y } end end
Se você rodar generate(:sentence) você obterá uma lista de 256 possíveis sentenças para está simples gramática. Neste caso o algoritmo é um pouco mais complicado. Ele também está usando o idioma Lisp de trabalhar no primeiro elemento de uma lista e então repetir-se no restante dela. Isto possibilita combinar tudo em conjunto. Assumo que seja possível criar algo convenientemente esperto baseado nos novos métodos Array#Permutations ou possivelmente Enumerable#group_by ou zip.
É interessante quão bem o uso de mappend e mapcar mapeiam para o uso de inject e map neste código.
Note que eu estive usando variáveis globais para as gramáticas nesta implementação. Uma alternativa que provavelmente é melhor é passar adiante um parâmetro opcional para os métodos. Se nenhuma gramática é fornecida, apenas use a constante padrão no lugar.
De qualquer modo, o código para este capítulo está no repositório. Brinque um pouco com ele e veja se você consegue encontrar algo interessante. Este código é definitivamente mais uma introdução a Lisp, do que um sério programa de IA - embora ele mostre o tipo de abordagens que têm sido utilizadas para geração primitiva de código.
Grupos de usuários de Ruby no Brasil (Ruby users groups in Brazil)
September 30th, 2008
Uma das coisas mais legais que noto na comunidade de Ruby norte-americana é que existem grupos locais de programadores Ruby como o de Seattle, o de Nova Iorque, de Houston, entre muitos outros.
Aqui no Brasil já vi que existem grupos de usuários de outras linguagens. Na próxima Conisli terá o ConaPHP e o YAPC Brasil (Yet Another Perl Conference). Na página do Python Brasil vejo que existe o grupy-sp, grupos de usuário do estado de SP) e recentemente aconteceu um encontro nacional da linguagem.
Acredito eu que não existem grupos de Ruby no Brasil, pelo menos não conheço. Só tive conhecimento de eventos ligados a Rails e nem sei dizer se se originaram de grupos.
A minha idéia é então criar grupos de usuários regionais. Poderíamos então fazer encontros mensais para trocar idéias, socializar, fazer desconferências, um evento anual. Algumas possíveis atividades são: trabalho em conjunto para melhorar alguma biblioteca do Ruby, produzir/traduzir documentação, realizar “coding dojos”, entre outros. O grupo também poderia manter uma biblioteca (de livros) comunitária e realizar minicursos de diversos assuntos ou algo parecido.
Acho que seria uma excelente maneira de fortalecermos a comunidade Ruby e mostrar que Ruby não é só Rails.
E aí, alguém compra essa idéia?
Obs.: Coloquei um tópico semelhante a este texto na lista de discussão Rails-Br
Atualização:
Minha mensagem surtiu algum efeito na lista de discussões Rails-BR e acabamos de criar o GURU-SP (Grupo de Usuários Ruby de São Paulo). Temos poucos membros ainda e estamos nos organizando aos poucos. Todos estão convidados a participar: iniciantes (n00bs) e também os hackers na linguagem.
Esse aqui é nosso logo provisório:

Para entrar no grupo, basta visitar nossa lista de discussões no Google Groups: Ruby-SP
