Há muito tempo li "What is Life?", um livro de uma famosa e polêmica bióloga chamada Lynn Margulis, mas apenas recentemente percebi alguns paralelos com os computadores. O livro descreve a vida de uma maneira muito diferente da que me foi apresentada na escola; lamento que eu tenha tido professores que se esforçaram para apresentar a biologia da maneira mais insossa possível.
Uma característica muito interessante da vida, segundo Margulis, é que ela evolui para saber cada vez mais a respeito de si própria. Os organismos unicelulares originais não sabiam se estavam no calor ou no frio. Não sabiam também se havia comida por perto. Eles simplesmente executavam o mínimo para sobreviver. Se não estavam no lugar certo, morriam.
Com o tempo, os seres vivos foram adquirindo meios de avaliar o ambiente e a si mesmos. As lesmas procuram a umidade. As baleias conseguem comunicar-se a centenas de quilometros de distância. Os cães conseguem detectar cheiros diluidos a uma parte por milhão. Até as bactérias conseguem saber se estão num ambiente adequado e movimentam-se de acordo.
Os nossos micros, no início, não detectavam nada sobre si mesmos e até a comunicação era difícil. Se um micro de 8 bits estivesse num ambiente quente, não pararia até derreter. Hoje em dia, os PCs têm vários sensores no hardware e publicam por SNMP seus dados vitais. Os micros em rede podem comunicar-se com pares do outro lado do mundo.
O que mais me impressionou no livro é como a vida é uma bagunça. As espécies não são distintas e isoladas de forma organizada. De fato, a vida só pode ser descrita como uma coisa só: a vida na terra. Um elemento não sobrevive sem todos os outros. Nós precisamos das bactérias para nossa digestão; precisamos das plantas para nosso oxigênio; dependemos de vários tipos de seres para nossa alimentação.
As nossas células são compostas por seres que já foram separados. As mitocôndrias geram a energia de nossas células, mas possuem seu próprio DNA, que tem muito em comum com o das bactérias.
Mesmo os primeiros computadores compartilhavam muitas peças. Os mesmos processadores e geradores de vídeo aparecem em centenas de micros; poucos micros de 8 bits não usam o 6502 ou o Z80. Mas hoje em dia avançamos ainda mais. O mesmo hardware pode ser reaproveitado de inúmeras maneiras. Pode-se trocar a placa de vídeo, os discos rígidos, o processador, a placa de som, etc.
As interações entre os seres num determinado momento geram o ambiente para o próximo e, aos poucos, o novo ambiente altera os próprios seres. Houve uma época em que não havia oxigênio. As cianobactérias, as algas e as plantas produzem oxigênio como resultado da fotossíntese. O oxigênio rapidamente junta-se a outros elementos e não haveria a concentração necessária para nossa sobrevivência se ele não fosse constantemente reposto. Então, o comportamento de uma geração de seres criou o ambiente necessário para o aparecimento das próximas gerações.
Os computadores modernos não podem ser fabricados sem o auxílio de computadores. Desenhar um micro de 8 bits com caneta e papel é possível, mas desenhar um processador de 64 bits não. Cada geração forneceu as ferramentas necessárias para a concepção da próxima.
Mas o que falta? A vida, aprendi, é um processo, não um indivíduo ou uma espécie. A vida constantemente se reconstrói e se reconfigura. As células estão constantemente se renovando em nossos corpos; quando uma espécie desaparece, outra logo ocupa seu espaço. A descrição da vida está no DNA e no RNA. No fundo, os corpos dos seres são o substrato para a sobrevivência do código genético. Inicialmente, era possível desenhar um computador sem o auxílio de software de CAD; no momento ele é imprescindível. Imagino que logo o processo todo poderá ser realizado por software, sendo o homem responsável por apenas ditar uma descrição geral do produto final. Em algum momento, será possível ao software desenhar o hardware necessário para executar a próxima geração de software.
quarta-feira, 30 de dezembro de 2009
segunda-feira, 28 de dezembro de 2009
Selecionando o que não está lá
Um problema interessante em SQL é o de selecionar o que não está na base. Esse problema se manifesta de diversas maneiras e eu vou analisar uma. Os exemplos abaixo usam a sintaxe do Oracle.
Digamos que eu tenha uma tabela de eventos, criativamente denominada EVENTOS, com as seguintes colunas:
Se eu quiser montar um calendário com o número de eventos por dia, posso escrever uma consulta como esta:
Essa consulta não vai mostrar os dias em que nada acontece. Isto é, ela não seleciona o que não está na base.
Se eu quiser mostrar todos os dias e um zero nos dias sem eventos, tenho que criar dados para poder selecioná-los. Freqüentemente, essa questão é resolvida com código, mas eu acredito que umas poucas linhas a mais em SQL economizam muitas linhas de Java ou qualquer que seja a linguagem da aplicação.
Em primeiro lugar, preciso de uma consulta que produza os dias que me interessam. A consulta abaixo, por exemplo, produz uma linha para cada dia do mês de fevereiro de 2008.
Qualquer tabela serve, desde que tenha tantas linhas quantos forem os dias que for preciso representar.
Agora, basta fazer um join com os dados:
O segundo where não pode estar no select principal, porque excluiria novamente os dias sem dados.
Para selecionar um período maior, é preciso um pouco de aritmética.
O primeiro where aumenta o número de dias para a diferença entre as datas do período. Para juntar as tabelas, é preciso subtrair da data do evento o primeiro dia e assim calcular a quantos dias do início do período cada evento está.
A solução para o problema, portanto, está em criar o que se quer selecionar.
Digamos que eu tenha uma tabela de eventos, criativamente denominada EVENTOS, com as seguintes colunas:
- DATA DATE
- DESC VARCHAR2(200)
Se eu quiser montar um calendário com o número de eventos por dia, posso escrever uma consulta como esta:
select count(1), data
from eventos
group by data
Essa consulta não vai mostrar os dias em que nada acontece. Isto é, ela não seleciona o que não está na base.
Se eu quiser mostrar todos os dias e um zero nos dias sem eventos, tenho que criar dados para poder selecioná-los. Freqüentemente, essa questão é resolvida com código, mas eu acredito que umas poucas linhas a mais em SQL economizam muitas linhas de Java ou qualquer que seja a linguagem da aplicação.
Em primeiro lugar, preciso de uma consulta que produza os dias que me interessam. A consulta abaixo, por exemplo, produz uma linha para cada dia do mês de fevereiro de 2008.
select rownum as dia
from eventos
where rownum<=to_char(last_day(to_date('022008', 'MMYYYY')), 'DD')
Qualquer tabela serve, desde que tenha tantas linhas quantos forem os dias que for preciso representar.
Agora, basta fazer um join com os dados:
select count(data), dia
from (
select rownum as dia
from eventos
where rownum<=to_char(last_day(to_date('022008', 'MMYYYY')), 'DD')
) dias left outer join (
select data
from eventos
where data between to_date('01022008', 'DDMMYYYY')
and to_date('28022008', 'DDMMYYYY')
) eventos on to_char(eventos.data, 'DD')=dias.dia
group by dia
order by dia
O segundo where não pode estar no select principal, porque excluiria novamente os dias sem dados.
Para selecionar um período maior, é preciso um pouco de aritmética.
select count(data), to_date('11022008')+dia-1
from (
select rownum as dia
from eventos
where rownum<=to_date('17032008', 'DDMMYYYY')
-to_date('11022008', 'DDMMYYYY')+1
) dias left outer join (
select data
from eventos
where data between to_date('11022008', 'DDMMYYYY')
and to_date('17032008', 'DDMMYYYY')
) eventos on eventos.data-to_date('11022008', 'DDMMYYYY')+1=dias.dia
group by dia
order by dia
O primeiro where aumenta o número de dias para a diferença entre as datas do período. Para juntar as tabelas, é preciso subtrair da data do evento o primeiro dia e assim calcular a quantos dias do início do período cada evento está.
A solução para o problema, portanto, está em criar o que se quer selecionar.
O Tarado do Access
Este ano houve muita polêmica sobre a queda da exigência de diploma superior para a carreira de jornalista. Eu acho que exigir um diploma superior é exagero para a maior parte das profissões. Exceto para a Medicina, a Engenharia Civil, a Odontologia e talvez algumas poucas outras, não é realmente necessário passar anos numa universidade.
Eu não excluo a possibilidade de que muitas profissões sejam regulamentadas, mas isso não tem nenhuma relação com o ensino superior. Os jornalistas, por exemplo, poderiam muito bem ter um conselho que regulasse as questões trabalhistas e éticas e ao qual os interessados se associariam livremente (ou através de uma prova, como os advogados).
Eu não acho a polêmica toda tão interessante, exceto por um detalhe. Descobri que as pessoas não consideram a informática uma profissão séria e digna de um diploma. Eu mesmo defendo que o diploma não é essencial, mas se ele for julgado crítico para o jornalismo, não vejo por que não o seria para a informática.
E agora eu apresento um personagem deveras problemático: o tarado do Access. Eu já testemunhei em duas organizações e já ouvi descrito no testemunho de outras pessoas o personagem que vou analisar a seguir.
O tarado do Access geralmente é um contador, mas não necessariamente. Ele produz pequenos aplicativos em Access ou Excel, com boas doses de VB. VB clássico, claro. Esses aplicativos costumam ser realmente úteis e logo são adotados por um grande número de pessoas.
Chega o dia, invariavelmente, em que o aplicativo (ou seus dados) precisa ser integrado à infraestrutura de TI da empresa. Nesse ponto, entra a informática e descobre que nenhuma de suas preocupações diárias foi considerada: modelagem de dados, infraestrutura de servidores, suporte aos usuários, segurança, integração com outros aplicativos, etc. O tarado do Access, é claro, não pode ajudar, porque já está ocupado com a próxima criação e também porque não tem a menor idéia do que seja preciso fazer.
Uma aplicação que eu mesmo vi, tinha em cada tabela uma coluna chamada INUTIL. Era a chave primária e o autor da obra não sabia para que servia, então deu-lhe este lindo nome. Eu acho que qualquer coisa chamada "chave primária" deve ter alguma importância, mas o óbvio é relativo.
Se houvesse a exigência de curso superior (ou mesmo de certificação) para a informática, poderíamos acusar essas pessoas (algumas até bem intencionadas) de execer ilegalmente a profissão. Mas é um pouco pesado para o simples o uso do Access, acho. Afinal, podem surgir situações perfeitamente legítimas, como a do dono de um pequeno estabelecimento que necessite de uma aplicação simples e isolada do mundo.
As organizações sérias não enviam correspondência mimeografada, nem fazem contabilidade em cadernos escolares, mesmo que a lei não os obrigue a isso ou que isso tenha conseqüências sérias. O que as menos sérias não perceberam ainda, é que deixar a informática para amadores coloca em risco seus próprios futuros. E se os dados forem perdidos ou roubados? E se os dados simplesmente não forem confiáveis, porque não foram normalizados?
Em algumas organizações que levam a sério o trabalho da informática, sabem-se quantos watts uma determinada aplicação consome. Ou quanto pesam os servidores. Ou quantos minutos são precisos para recompor o banco de dados, se houver uma falha catastrófica. São casos extremos, mas apontam para a importância da informática na infraestrutura tecnológica de hoje.
Eu creio que, em parte, as pessoas não levam a informática a sério, porque pensam que a compreendem. Elas usam um computador, usam uma planilha e até sabem montar uma apresentação no Powerpoint. Não é tão difícil! E aqueles incompetentes da TI levam meses para escrever uma aplicação que o tarado do Access monta numa semana.
Com tudo isso, continuo achando que não é preciso ter um diploma para a Informática, assim como para a maior parte das profissões. As organizações que permitem que o tarado do Access sobreviva não são organizações sérias e isso provavelmente irá se manifestar em outros aspectos de seu funcionamento. Logo, exigir diploma para a informática não vai salvá-las de sua própria incompetência.
Eu não excluo a possibilidade de que muitas profissões sejam regulamentadas, mas isso não tem nenhuma relação com o ensino superior. Os jornalistas, por exemplo, poderiam muito bem ter um conselho que regulasse as questões trabalhistas e éticas e ao qual os interessados se associariam livremente (ou através de uma prova, como os advogados).
Eu não acho a polêmica toda tão interessante, exceto por um detalhe. Descobri que as pessoas não consideram a informática uma profissão séria e digna de um diploma. Eu mesmo defendo que o diploma não é essencial, mas se ele for julgado crítico para o jornalismo, não vejo por que não o seria para a informática.
E agora eu apresento um personagem deveras problemático: o tarado do Access. Eu já testemunhei em duas organizações e já ouvi descrito no testemunho de outras pessoas o personagem que vou analisar a seguir.
O tarado do Access geralmente é um contador, mas não necessariamente. Ele produz pequenos aplicativos em Access ou Excel, com boas doses de VB. VB clássico, claro. Esses aplicativos costumam ser realmente úteis e logo são adotados por um grande número de pessoas.
Chega o dia, invariavelmente, em que o aplicativo (ou seus dados) precisa ser integrado à infraestrutura de TI da empresa. Nesse ponto, entra a informática e descobre que nenhuma de suas preocupações diárias foi considerada: modelagem de dados, infraestrutura de servidores, suporte aos usuários, segurança, integração com outros aplicativos, etc. O tarado do Access, é claro, não pode ajudar, porque já está ocupado com a próxima criação e também porque não tem a menor idéia do que seja preciso fazer.
Uma aplicação que eu mesmo vi, tinha em cada tabela uma coluna chamada INUTIL. Era a chave primária e o autor da obra não sabia para que servia, então deu-lhe este lindo nome. Eu acho que qualquer coisa chamada "chave primária" deve ter alguma importância, mas o óbvio é relativo.
Se houvesse a exigência de curso superior (ou mesmo de certificação) para a informática, poderíamos acusar essas pessoas (algumas até bem intencionadas) de execer ilegalmente a profissão. Mas é um pouco pesado para o simples o uso do Access, acho. Afinal, podem surgir situações perfeitamente legítimas, como a do dono de um pequeno estabelecimento que necessite de uma aplicação simples e isolada do mundo.
As organizações sérias não enviam correspondência mimeografada, nem fazem contabilidade em cadernos escolares, mesmo que a lei não os obrigue a isso ou que isso tenha conseqüências sérias. O que as menos sérias não perceberam ainda, é que deixar a informática para amadores coloca em risco seus próprios futuros. E se os dados forem perdidos ou roubados? E se os dados simplesmente não forem confiáveis, porque não foram normalizados?
Em algumas organizações que levam a sério o trabalho da informática, sabem-se quantos watts uma determinada aplicação consome. Ou quanto pesam os servidores. Ou quantos minutos são precisos para recompor o banco de dados, se houver uma falha catastrófica. São casos extremos, mas apontam para a importância da informática na infraestrutura tecnológica de hoje.
Eu creio que, em parte, as pessoas não levam a informática a sério, porque pensam que a compreendem. Elas usam um computador, usam uma planilha e até sabem montar uma apresentação no Powerpoint. Não é tão difícil! E aqueles incompetentes da TI levam meses para escrever uma aplicação que o tarado do Access monta numa semana.
Com tudo isso, continuo achando que não é preciso ter um diploma para a Informática, assim como para a maior parte das profissões. As organizações que permitem que o tarado do Access sobreviva não são organizações sérias e isso provavelmente irá se manifestar em outros aspectos de seu funcionamento. Logo, exigir diploma para a informática não vai salvá-las de sua própria incompetência.
terça-feira, 22 de dezembro de 2009
A máquina do tempo II
Durante a segunda grande guerra, o físico americano Richard P. Feynman trabalhou em Los Alamos em várias tarefas de suporte. Uma delas consistia em realizar algumas contas realmente complicadas. Ele decidiu que calcular manualmente era inviável e acabou optando por usar alguns equipamentos IBM. Naquela época, a IBM fabricava máquinas de cartões que somavam ou multiplicavam ou algumas outras operações.
Para montar um "computador", eram necessárias várias máquinas. A configuração e o uso determinavam o que elas produziriam. Colocavam-se os cartões numa e depois noutra e depois noutra e assim por diante.
Enquanto o equipamento não chegava, Feynman solicitou algumas secretárias e colocou-as sentadas frente a mesas com calculadoras Marchant. Cada uma faria o trabalho de uma máquina IBM.
A simulação foi um êxito. As moças trabalharam à velocidade prevista para as máquinas IBM. É claro que as máquinas IBM não cansariam depois de meia-hora.
Vamos avançar uns 60 anos.
Um PlayStation 3 é capaz de 218 GFLOPS. Digamos que todo o planeta (6 bilhões de pessoas) fosse colocado a fazer contas; supondo que cada pessoa fosse capaz de executar uma conta a cada 5 segundos, teriamos um pouco mais de 1 GFLOP.
Para montar um "computador", eram necessárias várias máquinas. A configuração e o uso determinavam o que elas produziriam. Colocavam-se os cartões numa e depois noutra e depois noutra e assim por diante.
Enquanto o equipamento não chegava, Feynman solicitou algumas secretárias e colocou-as sentadas frente a mesas com calculadoras Marchant. Cada uma faria o trabalho de uma máquina IBM.
A simulação foi um êxito. As moças trabalharam à velocidade prevista para as máquinas IBM. É claro que as máquinas IBM não cansariam depois de meia-hora.
Vamos avançar uns 60 anos.
Um PlayStation 3 é capaz de 218 GFLOPS. Digamos que todo o planeta (6 bilhões de pessoas) fosse colocado a fazer contas; supondo que cada pessoa fosse capaz de executar uma conta a cada 5 segundos, teriamos um pouco mais de 1 GFLOP.
segunda-feira, 21 de dezembro de 2009
Programação Incondicional III
As linguagens orientadas a objetos trouxeram à programação uma forma muito eficiente de eliminar ifs: o polimorfismo.
A habilidade de tratar homogeneamente objetos de classes diferentes ajuda a eliminar código como este:
A solução, evidentemente, é criar uma superclasse e colocar nela um método que as subclasses possam sobrescrever, sem que os clientes dessas classes tenham que se preocupar com qual método estão chamando. No seguinte exemplo, eu crio uma instância de Circle, mas uso uma referência a Shape (sua superclasse). Eu invoco draw() (que deve estar definido em Shape) e o método que vai ser invocado é o que está definido em Circle.
Se eu tiver um array de Shape, posso invocar draw() em cada elemento, sem considerar qual método estou realmente invocando.
No entanto, ainda há espaço para eliminar mais ifs! Uma característica da maior parte das linguagens orientadas a objetos é que o método invocado depende do tipo da referência usada, não do tipo do objeto. O seguinte exemplo ilustra bem o que quero dizer. Eu não tento invocar diretamente draw() em cada Shape, mas tento fazê-lo através de outro método draw() da própria classe Shape; eu poderia querer contar os tipos das instâncias ou tomar alguma outra ação.
No método main(), eu percorro um array do tipo Shape[] e invoco um método draw() para cada referência. Na classe Shape, há três métodos draw(); cada um recebe um tipo diferente de Shape. No entanto, como eu estou usando referências a Shape no laço, somente o método draw(Shape) é invocado.
Em algumas situações eu posso realmente desejar tomar uma ação diferente, conforme o tipo do objeto, não da referência. Nesse caso, eu estaria de volta a usar ifs e o operador instanceof.
Algumas poucas linguagens usam um tipo de invocação chamado double-dispatch. Nele, o tipo da referência não importa, mas sim o tipo do objeto. Em Java ou C#, não existe esse mecanismo, mas é possível emulá-lo com o padrão Visitor.
Então, vamos reescrever o exemplo acima. Vou criar um PaintVisitor e adicionar um método a Shape.
Finalmente, eu posso reescrever aquele laço da seguinte maneira:
E sem um if sequer!
Em algumas situações, o padrão Visitor pode ser realmente útil. Ao construir um editor de textos, eu descobri que era muito melhor separar o código de desenho dos objetos, porque era difícil dar o contexto necessário para cada objeto desenhar a si próprio. Um parágrafo, por exemplo, pode conter imagens e textos de diferentes tamanhos. Fazer cada elemento calcular sua posição e depois desenhar a si próprio, recursivamente, torna o código deveras complicado e extenso. A solução de criar um Visitor para calcular as posições e outro para desenhar o texto tornou o código muito menor e mais simples.
Quando for preciso percorrer uma estrutura de dados, vale a pena considerar o padrão Visitor.
A habilidade de tratar homogeneamente objetos de classes diferentes ajuda a eliminar código como este:
if(shape instanceof Circle) {
drawCircle();
} else if(shape instanceof Square) {
drawSquare();
}
A solução, evidentemente, é criar uma superclasse e colocar nela um método que as subclasses possam sobrescrever, sem que os clientes dessas classes tenham que se preocupar com qual método estão chamando. No seguinte exemplo, eu crio uma instância de Circle, mas uso uma referência a Shape (sua superclasse). Eu invoco draw() (que deve estar definido em Shape) e o método que vai ser invocado é o que está definido em Circle.
Shape s=new Circle();
s.draw();
Se eu tiver um array de Shape, posso invocar draw() em cada elemento, sem considerar qual método estou realmente invocando.
for(int i=0; i<shapes.length; i++) {
shapes[i].draw();
}
No entanto, ainda há espaço para eliminar mais ifs! Uma característica da maior parte das linguagens orientadas a objetos é que o método invocado depende do tipo da referência usada, não do tipo do objeto. O seguinte exemplo ilustra bem o que quero dizer. Eu não tento invocar diretamente draw() em cada Shape, mas tento fazê-lo através de outro método draw() da própria classe Shape; eu poderia querer contar os tipos das instâncias ou tomar alguma outra ação.
import static java.lang.System.out;
public class Shape {
private static int circles, squares;
public void draw() {
//
}
public static void draw(Shape s) {
out.println("I drew a shape");
}
public static void draw(Circle s) {
out.println("I drew a circle");
circles++
}
public static void draw(Square s) {
out.println("I drew a square");
squares++;
}
public static void main(String... args) {
Shape[] shapes=new Shape[] { new Circle(), new Square() };
for(int i=0; i<shapes.length; i++) {
draw(shapes[i]);
}
}
}
class Circle extends Shape {
public void draw() {
//
}
}
class Square extends Shape {
public void draw() {
//
}
}
No método main(), eu percorro um array do tipo Shape[] e invoco um método draw() para cada referência. Na classe Shape, há três métodos draw(); cada um recebe um tipo diferente de Shape. No entanto, como eu estou usando referências a Shape no laço, somente o método draw(Shape) é invocado.
Em algumas situações eu posso realmente desejar tomar uma ação diferente, conforme o tipo do objeto, não da referência. Nesse caso, eu estaria de volta a usar ifs e o operador instanceof.
Algumas poucas linguagens usam um tipo de invocação chamado double-dispatch. Nele, o tipo da referência não importa, mas sim o tipo do objeto. Em Java ou C#, não existe esse mecanismo, mas é possível emulá-lo com o padrão Visitor.
Então, vamos reescrever o exemplo acima. Vou criar um PaintVisitor e adicionar um método a Shape.
public class Shape {
public void accept(PaintVisitor v) {
visit(this);
}
}
class Circle extends Shape {
public void accept(PaintVisitor v) {
v.visit(this);
}
}
class Square extends Shape {
public void accept(PaintVisitor v) {
v.visit(this);
}
}
class PaintVisitor {
public void visit(Shape s) {
out.println("I drew a shape");
}
public void visit(Circle c) {
out.println("I drew a circle");
}
public void visit(Square s) {
out.println("I drew a square");
}
public void draw(Shape s) {
s.accept(this);
}
}
Finalmente, eu posso reescrever aquele laço da seguinte maneira:
PaintVisitor visitor=new PaintVisitor();
for(int i=0; i<shapes.length; i++) {
visitor.draw(shapes[i]);
}
E sem um if sequer!
Em algumas situações, o padrão Visitor pode ser realmente útil. Ao construir um editor de textos, eu descobri que era muito melhor separar o código de desenho dos objetos, porque era difícil dar o contexto necessário para cada objeto desenhar a si próprio. Um parágrafo, por exemplo, pode conter imagens e textos de diferentes tamanhos. Fazer cada elemento calcular sua posição e depois desenhar a si próprio, recursivamente, torna o código deveras complicado e extenso. A solução de criar um Visitor para calcular as posições e outro para desenhar o texto tornou o código muito menor e mais simples.
Quando for preciso percorrer uma estrutura de dados, vale a pena considerar o padrão Visitor.
sexta-feira, 18 de dezembro de 2009
Programação Incondicional II
Em 1987, a Acorn lançou um micro de 32 bits impressionante: o Archimedes. Era o micro mais rápido e rodava a 4 MIPS. O processador tinha sido desenvolvido pela própria Acorn e chama-se ARM (Acorn RISC Machine). Hoje em dia, os processadores ARM estão por todas as partes, porque são usados em celulares. Como a Acorn não existe mais, a sigla passou a representar Advanced RISC Machine.
A primeira aparição do ARM foi como um segundo processador para o BBC. Como o BBC tinha um barramento (The Tube) de 1MHz especial para conectar outros processadores, havia uma oferta bem interessante de opções: Z80, 8086, 32016, etc. O mais comum, era ligar um Z80 para rodar CP/M ou um segundo 6502 a 4Mhz para rodar programas com mais velocidade e com mais memória (como o processador principal ficava encarregado do I/O, o secundário deixava até 44KB de memória livre para programas em BASIC).
Pois bem, com o contexto histórico estabelecido, vamos aos detalhes técnicos relevantes. O ARM é uma arquitetura RISC que faz uso de pipelines para melhorar o desempenho. E num processador com pipeline (acho que hoje em dia, todos têm), as instruções mais danosas são as de branch.
Para evitar quebrar o pipeline, os projetistas do ARM decidiram reaproveitar a lógica de comparação em todas as intruções. Em assembly de 6502, para decidir quando fazer um pulo, faz-se o seguinte:
No ARM, cada instrução pode ser complementada com um teste.
Vamos comparar dois trechos de código que produzem o mesmo resultado. Em primeiro lugar, código com pulos tradicionais. Este é um algoritmo para encontrar o maior denominador comum:
Usando as instruções condicionais, o código fica muito mais compacto.
No segundo trecho, as instruções de subtração são complementadas com gt e lt. Conforme o resultado da instrução cmp, será executada a primeira ou a segunda subtração. Uma instrução que não é executada consome um ciclo, mas não quebra o pipeline. Além disso, o código fica muito mais compacto.
Estão prometidos há algum tempo netbooks com processadores ARM. Mal posso esperar!
A primeira aparição do ARM foi como um segundo processador para o BBC. Como o BBC tinha um barramento (The Tube) de 1MHz especial para conectar outros processadores, havia uma oferta bem interessante de opções: Z80, 8086, 32016, etc. O mais comum, era ligar um Z80 para rodar CP/M ou um segundo 6502 a 4Mhz para rodar programas com mais velocidade e com mais memória (como o processador principal ficava encarregado do I/O, o secundário deixava até 44KB de memória livre para programas em BASIC).
Pois bem, com o contexto histórico estabelecido, vamos aos detalhes técnicos relevantes. O ARM é uma arquitetura RISC que faz uso de pipelines para melhorar o desempenho. E num processador com pipeline (acho que hoje em dia, todos têm), as instruções mais danosas são as de branch.
Para evitar quebrar o pipeline, os projetistas do ARM decidiram reaproveitar a lógica de comparação em todas as intruções. Em assembly de 6502, para decidir quando fazer um pulo, faz-se o seguinte:
CMP #FF ;Compara o acumulador A com &FF
BEQ .next ;Pula para .next se igual (Branch if EQual)
No ARM, cada instrução pode ser complementada com um teste.
Vamos comparar dois trechos de código que produzem o mesmo resultado. Em primeiro lugar, código com pulos tradicionais. Este é um algoritmo para encontrar o maior denominador comum:
gcd cmp r0, r1 ;Terminou?
beq stop
blt less ;se r0 > r1
sub r0, r0, r1 ;subtrai r1 de r0
bal gcd
less sub r1, r1, r0 ;subtrai r0 de r1
bal gcd
stop
Usando as instruções condicionais, o código fica muito mais compacto.
gcd cmp r0, r1 ;se r0 > r1
subgt r0, r0, r1 ;subtrai r1 de r0
sublt r1, r1, r0 ;ou subtrai r0 de r1
bne gcd ;terminou?
No segundo trecho, as instruções de subtração são complementadas com gt e lt. Conforme o resultado da instrução cmp, será executada a primeira ou a segunda subtração. Uma instrução que não é executada consome um ciclo, mas não quebra o pipeline. Além disso, o código fica muito mais compacto.
Estão prometidos há algum tempo netbooks com processadores ARM. Mal posso esperar!
quinta-feira, 17 de dezembro de 2009
Programação Incondicional
Não faltam metodologias, plataformas e tecnologias, mas resolvi criar mais uma filosofia de programação mesmo assim. A programação incondicional (ifless programming ou unconditional programming) não é um novo radicalismo. É apenas uma tentativa de reduzir bugs através do seu maior vetor: o if.
Todas as linguagens de programação possuem ifs (exceto Prolog). Parecem inevitáveis, mas, na maior parte dos casos, é possível eliminar muitos ifs sem dificuldades. Vou mostrar como.
Uma técnica muito simples (e talvez a mais antiga encarnação da programação incondicional) é o array de ponteiros para funções. É tão simples, que pode ser usada em Assembly, como um array de endereços.
Em C, posso declarar um array de funções que recebem dois ints e retornam int assim:
Posso invocar qualquer uma das funções assim:
E com isso evito a seguinte seqüência particulamente perniciosa de ifs:
Perl oferece uma das sintaxes mais simples para declarar estruturas de dados; posso declarar o array e as funções de uma só vez de maneira bastante compacta.
Java, infelizmente, não possui funções, mas elas podem ser simuladas com subclasses. Como no Perl, é possível criar as subclasses e o array ao mesmo tempo, mas a sintaxe é muito mais complicada.
Usar um hash (array associativo) é só um caso especial dessa técnica. Nenhuma linguagem permite um exemplo tão compacto como Perl:
E esta foi a introdução à programação incondicional!
Todas as linguagens de programação possuem ifs (exceto Prolog). Parecem inevitáveis, mas, na maior parte dos casos, é possível eliminar muitos ifs sem dificuldades. Vou mostrar como.
Uma técnica muito simples (e talvez a mais antiga encarnação da programação incondicional) é o array de ponteiros para funções. É tão simples, que pode ser usada em Assembly, como um array de endereços.
Em C, posso declarar um array de funções que recebem dois ints e retornam int assim:
int (*p[4])(int x, int y);
Posso invocar qualquer uma das funções assim:
resultado=(*p[op]) (i, j);
E com isso evito a seguinte seqüência particulamente perniciosa de ifs:
if(op==0) {
resultado=somar(i, j);
} else if(op==1) {
resultado=subtrair(i, j);
} else if(op==2) {
resultado=dividir(i, j);
} else ...
Perl oferece uma das sintaxes mais simples para declarar estruturas de dados; posso declarar o array e as funções de uma só vez de maneira bastante compacta.
my @ops = (
sub { $_[0]+$_[1] },
sub { $_[0]-$_[1] },
sub { $_[0]/$_[1] },
sub { $_[0]*$_[1] },
);
my $resultado=$ops[2]->(3,2);
Java, infelizmente, não possui funções, mas elas podem ser simuladas com subclasses. Como no Perl, é possível criar as subclasses e o array ao mesmo tempo, mas a sintaxe é muito mais complicada.
Operador[] ops=new Operador[] {
new Operador() {
public int eval(int i, int j) {
return i+j;
}
},
new Operador() {
public int eval(int i, int j) {
return i-j;
}
},
//etc, etc, etc...
};
int resultado=ops[2].eval(3,2);
Usar um hash (array associativo) é só um caso especial dessa técnica. Nenhuma linguagem permite um exemplo tão compacto como Perl:
my %ops = (
'+' => sub { $_[0]+$_[1] },
'-' => sub { $_[0]-$_[1] },
'/' => sub { $_[0]/$_[1] },
'*' => sub { $_[0]*$_[1] },
);
my $resultado=$ops{'+'}->(3,2);
E esta foi a introdução à programação incondicional!
terça-feira, 15 de dezembro de 2009
Perlismos em Java II
No primeiro "Perlismos" eu criei uma classe para emular a função grep do Perl. Eu devia ter seguido melhor a sabedoria do camelo e usado a mesma ordem dos parâmetros, porque logo percebi que com varargs seria possível fazer algo ainda mais prático.
Com isso, posso fazer chamadas mesmo quando não tiver uma lista. Não gosto de arbitrariamente criar estruturas só para poder usar um método. Com vargars, a segunda versão de grep() pode receber uma seqüência qualquer de elementos, como no exemplo abaixo:
import java.util.*;
public class Grep {
public static <T> List<T> grep(Grepping<T> mapping, List<T> in) {
List<T> out=new ArrayList<T>();
for(T t : in) {
if(mapping.map(t)) {
out.add(t);
}
}
return out;
}
public static <T> List<T> grep(Grepping<T> mapping, T... in) {
List<T> out=new ArrayList<T>();
if(in!=null) {
for(int i=0; i<in.length; i++) {
if(mapping.map(in[i])) {
out.add(in[i]);
}
}
}
return out;
}
}
Com isso, posso fazer chamadas mesmo quando não tiver uma lista. Não gosto de arbitrariamente criar estruturas só para poder usar um método. Com vargars, a segunda versão de grep() pode receber uma seqüência qualquer de elementos, como no exemplo abaixo:
mulheres=Grep.grep(PessoaGrepping.MULHERES,
fulano, ciclana, beltrano, arlinda);
Leitura: O homem que roubou Portugal
O Homem que Roubou Portugal
Murray Teigh Bloom
Jorge Zahar Ed.
ISBN 978-85-378-0111-6
Trabalhei uma vez num projeto web em Java e JSP que usava uma página cheia de código para implementar um controle de acesso integrado com o Windows. Eu achei muito engraçado que a página não abria comunicação com mais ninguém, além do cliente.
Como poderia a página verificar as credenciais do usuário? Um rápido teste revelou que não verificava. Então, eu troquei a página por um filtro com o jCIF e passei a gravar as tentativas de entrar no sistema. Imediatamente percebi que mais de um usuário tentou burlar a segurança. O desenvolvedor original simplesmente havia testado o controle apenas com senhas corretas; seus usuários logo descobriram como burlar a falha.
Pois este livro que estou indicando não é sobre informática, mas tem tantas analogias com segurança de rede (além de ser muito divertido para quem gosta de história e finanças) que tem seu lugar nesse blog.
O livro conta a história de um empreendedor falido de Portugal, Alves Reis, que, com a ajuda de alguns comparças, aplicou o maior golpe financeiro de todos os tempos.
Inicialmente, ele falsificou uma carta de um dirigente do banco central lusitano para que uma firma inglesa (a Waterloo and Sons Ltd.) imprimisse notas. Como as notas já estavam sendo impressas por essa firma, elas seriam idênticas às legitimas.
Eis o primeiro paralelo com segurança de redes e o caso da autenticação integrada do Windows. A firma inglesa confiou na carta que recebeu, sem exigir mais comprovações diretamente à suposta fonte.
Um problema interessante que Alves Reis enfrentou foi o de como determinar a numeração das cédulas. A regra era guardada pelo banco central e mesmo com uma amostra grande de notas, não havia como ter certeza. É como analisar pacotes para determinar um protocolo.
Uma vez que estava bem estabelecido o golpe e a firma de impressão já confiava nele, Alves Reis tentou comprar ações suficientes do banco central para tomar controle da instituição. Tendo acesso ao banco central, ele poderia encobrir completamente o seu esquema. Ele queria privilégios de administrador.
Alves Reis adquiriu vários negócios e fundou o Banco de Angola e Metrópole, com o objetivo declarado de promover o desenvolvimento da colônia.
Pois, o fato mais impressionante dessa história é que o derrame de notas teve uma influência positiva na economia estagnada de Portugal. Pode-se dizer que Reis era um falsário keynesiano. As notas de Reis somaram quase 1% do PIB lusitano.
No todo, é uma leitura muito interessante e divertida. Alves Reis acabou preso, mas teve uma vida notável.
Murray Teigh Bloom
Jorge Zahar Ed.
ISBN 978-85-378-0111-6
Trabalhei uma vez num projeto web em Java e JSP que usava uma página cheia de código para implementar um controle de acesso integrado com o Windows. Eu achei muito engraçado que a página não abria comunicação com mais ninguém, além do cliente.
Como poderia a página verificar as credenciais do usuário? Um rápido teste revelou que não verificava. Então, eu troquei a página por um filtro com o jCIF e passei a gravar as tentativas de entrar no sistema. Imediatamente percebi que mais de um usuário tentou burlar a segurança. O desenvolvedor original simplesmente havia testado o controle apenas com senhas corretas; seus usuários logo descobriram como burlar a falha.
Pois este livro que estou indicando não é sobre informática, mas tem tantas analogias com segurança de rede (além de ser muito divertido para quem gosta de história e finanças) que tem seu lugar nesse blog.
O livro conta a história de um empreendedor falido de Portugal, Alves Reis, que, com a ajuda de alguns comparças, aplicou o maior golpe financeiro de todos os tempos.
Inicialmente, ele falsificou uma carta de um dirigente do banco central lusitano para que uma firma inglesa (a Waterloo and Sons Ltd.) imprimisse notas. Como as notas já estavam sendo impressas por essa firma, elas seriam idênticas às legitimas.
Eis o primeiro paralelo com segurança de redes e o caso da autenticação integrada do Windows. A firma inglesa confiou na carta que recebeu, sem exigir mais comprovações diretamente à suposta fonte.
Um problema interessante que Alves Reis enfrentou foi o de como determinar a numeração das cédulas. A regra era guardada pelo banco central e mesmo com uma amostra grande de notas, não havia como ter certeza. É como analisar pacotes para determinar um protocolo.
Uma vez que estava bem estabelecido o golpe e a firma de impressão já confiava nele, Alves Reis tentou comprar ações suficientes do banco central para tomar controle da instituição. Tendo acesso ao banco central, ele poderia encobrir completamente o seu esquema. Ele queria privilégios de administrador.
Alves Reis adquiriu vários negócios e fundou o Banco de Angola e Metrópole, com o objetivo declarado de promover o desenvolvimento da colônia.
Pois, o fato mais impressionante dessa história é que o derrame de notas teve uma influência positiva na economia estagnada de Portugal. Pode-se dizer que Reis era um falsário keynesiano. As notas de Reis somaram quase 1% do PIB lusitano.
No todo, é uma leitura muito interessante e divertida. Alves Reis acabou preso, mas teve uma vida notável.
segunda-feira, 14 de dezembro de 2009
Perlismos em Java
Linguagens dinâmicas como Perl costumam ter algumas pequenas facilidades que reduzem significativamente a quantidade de código necessária para executar operações com strings, listas e hashes. É difícil ou impossível recriá-las em linguagens mais rígidas, como Java ou C.
Mesmo assim, acho que vale a pena investigar o que pode ser reaproveitado. Vou começar com grep e map, que são funções que processam listas. Grep é usado para selecionar itens de uma lista e map para mapear uma lista para outra.
Em Perl, grep recebe dois parâmetros: um bloco de código (ou uma expressão) e uma lista. Para cada elemento da lista, grep executa o bloco ou avalia a expressão. Se o resultado for verdadeiro, o elemento é adicionado à lista de saída. O exemplo abaixo é do manual do ActivePerl:
Este código mostra uma chamada a grep com uma expressão regular (seleciona linhas que não começam com #) e uma lista @bar.
Java, infelizmente, não tem funções propriamente ditas, então vai ser preciso escrever grep como um método estático:
O código usa generics, então parece mais complicado do que é. Em poucas palavras, ela recebe uma lista de elementos do tipo T e um mapeamento para elementos desse mesmo tipo; ela retorna uma lista do mesmo tipo T.
A classe Grepping é bastante simples e já fornece um comportamento default (sempre retorna true):
Suponha que seja preciso selecionar um grupo de pessoas, conforme a idade. Precisamos de uma classe para pessoas e outra para lista de pessoas.
Com isso, posso escrever um mapeamento por idade.
Então, seu eu quiser selecionar todos os maiores de 18 anos, posso fazer o seguinte:
Nas situações em que o mapeamento não precisa ser parametrizado, gosto de criar um membro estático assim:
Usar os mapeamentos é simples:
Para simular a função map, basta criar uma classe Mapping, que retorne uma instância de T no lugar de um booleano:
O resultado não é tão simples como o que se pode escrever em Perl, mas já é um avanço. Quando Java tiver closures, certamente será possível escrever código ainda mais compacto para executar essas funções.
Mesmo assim, acho que vale a pena investigar o que pode ser reaproveitado. Vou começar com grep e map, que são funções que processam listas. Grep é usado para selecionar itens de uma lista e map para mapear uma lista para outra.
Em Perl, grep recebe dois parâmetros: um bloco de código (ou uma expressão) e uma lista. Para cada elemento da lista, grep executa o bloco ou avalia a expressão. Se o resultado for verdadeiro, o elemento é adicionado à lista de saída. O exemplo abaixo é do manual do ActivePerl:
@foo=grep(!/^#/,@bar);# weed out comments
Este código mostra uma chamada a grep com uma expressão regular (seleciona linhas que não começam com #) e uma lista @bar.
Java, infelizmente, não tem funções propriamente ditas, então vai ser preciso escrever grep como um método estático:
import java.util.*;
public class Grep {
public static <T> List<T> grep(List<T> in, Grepping<T> mapping) {
List<T> out=new ArrayList<T>();
for(T t : in) {
if(mapping.map(t)) {
out.add(t);
}
}
return out;
}
}
O código usa generics, então parece mais complicado do que é. Em poucas palavras, ela recebe uma lista de elementos do tipo T e um mapeamento para elementos desse mesmo tipo; ela retorna uma lista do mesmo tipo T.
A classe Grepping é bastante simples e já fornece um comportamento default (sempre retorna true):
public class Grepping<T> {
public boolean map(T t) {
return true;
}
}
Suponha que seja preciso selecionar um grupo de pessoas, conforme a idade. Precisamos de uma classe para pessoas e outra para lista de pessoas.
public class Pessoa {
private String nome;
private int idade;
private char sexo;
public Pessoa(String nome, int idade, char sexo) {
this.nome=nome;
this.idade=idade;
this.sexo;
}
public String nome() {
return nome;
}
public int idade() {
return idade;
}
public char getSexo() {
return sexo;
}
}
public class PessoaList extends ArrayList<Pessoa> {
}
Com isso, posso escrever um mapeamento por idade.
public class PessoaPorIdade extends Grepping<Pessoa> {
private int limiar;
public PessoaPorIdade(int limiar) {
this.limiar=limiar;
}
public boolean map(Pessoa p) {
return p.getIdade()>=limiar;
}
}
Então, seu eu quiser selecionar todos os maiores de 18 anos, posso fazer o seguinte:
maiores=Grep.grep(pessoas, new PessoasPorIdade(18));
Nas situações em que o mapeamento não precisa ser parametrizado, gosto de criar um membro estático assim:
public class PessoaGrepping {
public static final Grepping<Pessoa> MULHERES=
new Grepping<Pessoa>() {
public boolean map(Pessoa p) {
return p.getSexo()='F';
}
}
public static final Grepping<Pessoa> HOMENS=
new Grepping<Pessoa>() {
public boolean map(Pessoa p) {
return p.getSexo()='M';
}
}
}
Usar os mapeamentos é simples:
mulheres=Grep.grep(pessoas, PessoaGrepping.MULHERES);
Para simular a função map, basta criar uma classe Mapping, que retorne uma instância de T no lugar de um booleano:
import java.util.*;
public class Map{
public static <T> List<T> map(List<T> in, Mapping<T> mapping) {
List<T> out=new ArrayList<T>();
for(T t : in) {
T r=mapping.map(t);
if(r!=null) {
out.add(r);
}
}
return out;
}
}
public class Mapping<T> {
public T map(T t) {
return t;
}
}
O resultado não é tão simples como o que se pode escrever em Perl, mas já é um avanço. Quando Java tiver closures, certamente será possível escrever código ainda mais compacto para executar essas funções.
quarta-feira, 9 de dezembro de 2009
Invocando procedures com Java
Há uma tendência no mundo Java de exagerar no uso de frameworks. Não raramente, encontro sistemas que têm mais frameworks que telas, ou mais XML que Java. O Hibernate é dos piores infratores.
O problema de ter tanta configuração em XML é que ele sequer costuma diminuir a quantidade de código. Costuma haver, isso sim, uma troca. Troca-se Java por XML. Ou SQL por XML. Qual será a vantagem de trocar uma linguagem com tantos controles por algo que só é verificado em tempo de execução?
Pois, eu argumento que as consultas são melhor escritas em SQL e colocadas em procedures. E meu argumento principal é o de que isso torna muito mais fácil a vida do DBA. Estando as consultas em procedures e functions, ele pode facilmente encontrar e corrigir problemas de desempenho. Além disso, as consultas podem ser compartilhadas por diferentes sistemas; dados costumam viver mais que os sistemas que os utilizam (e raramente um banco de dados é usado por apenas uma aplicação).
Tendo tudo isso em mente, fiquei muito feliz quando Java finalmente adotou métodos com número variável de parâmetros (varargs) e autoboxing. Com a JDK 1.5, finalmente foi possível escrever uma interface realmente simples e direta para invocar procedures e functions. Para resolver um problema, é muito melhor criar uma boa abstração do que iludir-se com XML.
Considere as duas classes abstratas abaixo:
Eu não usei interfaces, porque os gets não dependem da implementação específica e, portanto, podem ser codificados na superclasse e aproveitadas na implementação para cada banco de dados.
O que eu almejei foi poder escrever algo como:
O construtor de OracleProcedure recebe dois parâmetros:
Para o método call(), passo os parâmetros exigidos pela procedure. Como o segundo parâmetro é de saída (OUT), uso null naquela posição. As instâncias de Procedure e Function têm todos os gets de java.sql.CallableStatement, de tal sorte que é possível pegar os parâmetros de saída sem dificuldades.
Na primeira vez que uma procedure é invocada, OracleProcedure (OracleFunction faz o mesmo) recupera os metadados (tipos dos parâmetros) e os guarda num Map, para não ter que repetir esse passo. Em seguida, o código monta a consulta (neste caso, "{call PESSOAS_PKG.BUSCAR_PESSOA_PROC(?,?)}") e também a guarda. Um CallableStatement é criado, os parâmetros são atribuídos (é fácil escolher o setXXX() adequado com base no tipo do parâmetro) e a consulta, finalmente, é executada.
Nas primeiras versões dessas classes, eu também guardava a referência à datasource, para evitar ter que fazer outra pesquisa JNDI. Desisti de fazer isso porque as implementações do JNDI já são rápidas o suficiente para tornar a economia de tempo desprezível. Além disso, era impossível alterar ou reiniciar a datasource sem reiniciar a aplicação.
Com isso, tenho uma maneira simples de invocar procedures e functions. As camadas em Java ficam bastante mais simples e livres de XML. O código de acesso a dados fica restrito ao banco e pode ser compartilhado com outras aplicações.
O problema de ter tanta configuração em XML é que ele sequer costuma diminuir a quantidade de código. Costuma haver, isso sim, uma troca. Troca-se Java por XML. Ou SQL por XML. Qual será a vantagem de trocar uma linguagem com tantos controles por algo que só é verificado em tempo de execução?
Pois, eu argumento que as consultas são melhor escritas em SQL e colocadas em procedures. E meu argumento principal é o de que isso torna muito mais fácil a vida do DBA. Estando as consultas em procedures e functions, ele pode facilmente encontrar e corrigir problemas de desempenho. Além disso, as consultas podem ser compartilhadas por diferentes sistemas; dados costumam viver mais que os sistemas que os utilizam (e raramente um banco de dados é usado por apenas uma aplicação).
Tendo tudo isso em mente, fiquei muito feliz quando Java finalmente adotou métodos com número variável de parâmetros (varargs) e autoboxing. Com a JDK 1.5, finalmente foi possível escrever uma interface realmente simples e direta para invocar procedures e functions. Para resolver um problema, é muito melhor criar uma boa abstração do que iludir-se com XML.
Considere as duas classes abstratas abaixo:
public abstract class Procedure {
public abstract void call(Object... args)
throws SQLException, IOException;
public abstract void close();
public String getString(int columnIndex) throws SQLException {
return getCallable().getString(columnIndex);
}
public String getString(String columnName) throws SQLException {
return getCallable().getString(columnName);
}
//demais gets omitidos
}
public abstract class Function {
public abstract Object call(Object... args) throws SQLException;
public abstract void close();
//gets omitidos
}
Eu não usei interfaces, porque os gets não dependem da implementação específica e, portanto, podem ser codificados na superclasse e aproveitadas na implementação para cada banco de dados.
O que eu almejei foi poder escrever algo como:
Procedure p=null;
try {
//BUSCAR_PESSOA_PROC(
// P_CODIGO IN NUMBER,
// P_CURSOR OUT SYS_REFCURSOR
//);
p=new OracleProcedure("java:comp/env/jdbc/data",
"PESSOAS_PKG.BUSCAR_PESSOA_PROC");
p.call(123, null);
ResultSet rs=p.getCursor(1);
while(rs.next()) {
//percorrer cursor
}
} catch(Exception e) {
e.printStackTrace();
} finally {
p.close();
}
O construtor de OracleProcedure recebe dois parâmetros:
- O nome de uma datasource;
- O nome de uma procedure (com ou sem package);
Para o método call(), passo os parâmetros exigidos pela procedure. Como o segundo parâmetro é de saída (OUT), uso null naquela posição. As instâncias de Procedure e Function têm todos os gets de java.sql.CallableStatement, de tal sorte que é possível pegar os parâmetros de saída sem dificuldades.
Na primeira vez que uma procedure é invocada, OracleProcedure (OracleFunction faz o mesmo) recupera os metadados (tipos dos parâmetros) e os guarda num Map, para não ter que repetir esse passo. Em seguida, o código monta a consulta (neste caso, "{call PESSOAS_PKG.BUSCAR_PESSOA_PROC(?,?)}") e também a guarda. Um CallableStatement é criado, os parâmetros são atribuídos (é fácil escolher o setXXX() adequado com base no tipo do parâmetro) e a consulta, finalmente, é executada.
Nas primeiras versões dessas classes, eu também guardava a referência à datasource, para evitar ter que fazer outra pesquisa JNDI. Desisti de fazer isso porque as implementações do JNDI já são rápidas o suficiente para tornar a economia de tempo desprezível. Além disso, era impossível alterar ou reiniciar a datasource sem reiniciar a aplicação.
Com isso, tenho uma maneira simples de invocar procedures e functions. As camadas em Java ficam bastante mais simples e livres de XML. O código de acesso a dados fica restrito ao banco e pode ser compartilhado com outras aplicações.
Primeiro encontro com um PC
Nem lembro mais quando foi meu primeiro encontro com um PC. Foi, provavelmente, na UFSM (Universidade Federal de Santa Maria). Lembro claramente, isso sim, que foi uma tremenda decepção.
Os primeiros PCs muitas vezes não tinham disco rígido, embora sempre tivessem ao menos um drive de disquete. Então, quando se ligava um deles, não havia nenhuma resposta, exceto o pedido de inserir um disquete de sistema operacional.
Ora, todos os micros de 8 bits, quando ligados, imediatamente exibiam um prompt e ofereciam a possibilidade de digitar programas em BASIC. E como eram rápidos! O sistema operacional residia em ROM, mapeado para uma área reservada de memória, então não era necessário nem transferi-lo de um lugar a outro. No Apple II, por exemplo, os últimos 16KB do espaço de endereçamento eram reservados ao sistema operacional; os primeiros 48KB eram ocupados por RAM.
No BBC B, os primeiros 32KB eram de RAM, seguidos por 16KB reservados para ROMs (era possível paginar entre qualquer um de 16 ROMs) e, finalmente, os últimos 16KB pertenciam ao MOS (Machine Operating System). O BBC B+ tinha mais 32KB de shadow RAM e o BBC B+ 128KB tinha ainda mais 64KB de RAM separados em 4 bancos de 16KB, que ocupavam slots junto com aqueles ROMs paginados (era o sideways RAM).
Dentre esses ROMs, minha máquina tinha um processador de textos e um Pascal com editor. Então, quando eu a ligava, podia imediatamente escrever um programa em BASIC ou Pascal, ou editar um texto no Wordwise.
O que eu podia fazer num PC? Podia inserir o disquete do DOS. E depois disso? Ainda não podia fazer nada. Era preciso inserir um disquete com algum programa. Só então era possível fazer algo interessante. E, mesmo assim, esse algo interessante tinha que ser verde, porque o monitor só tinha essa cor.
Os primeiros PCs muitas vezes não tinham disco rígido, embora sempre tivessem ao menos um drive de disquete. Então, quando se ligava um deles, não havia nenhuma resposta, exceto o pedido de inserir um disquete de sistema operacional.
Ora, todos os micros de 8 bits, quando ligados, imediatamente exibiam um prompt e ofereciam a possibilidade de digitar programas em BASIC. E como eram rápidos! O sistema operacional residia em ROM, mapeado para uma área reservada de memória, então não era necessário nem transferi-lo de um lugar a outro. No Apple II, por exemplo, os últimos 16KB do espaço de endereçamento eram reservados ao sistema operacional; os primeiros 48KB eram ocupados por RAM.
No BBC B, os primeiros 32KB eram de RAM, seguidos por 16KB reservados para ROMs (era possível paginar entre qualquer um de 16 ROMs) e, finalmente, os últimos 16KB pertenciam ao MOS (Machine Operating System). O BBC B+ tinha mais 32KB de shadow RAM e o BBC B+ 128KB tinha ainda mais 64KB de RAM separados em 4 bancos de 16KB, que ocupavam slots junto com aqueles ROMs paginados (era o sideways RAM).
Dentre esses ROMs, minha máquina tinha um processador de textos e um Pascal com editor. Então, quando eu a ligava, podia imediatamente escrever um programa em BASIC ou Pascal, ou editar um texto no Wordwise.
O que eu podia fazer num PC? Podia inserir o disquete do DOS. E depois disso? Ainda não podia fazer nada. Era preciso inserir um disquete com algum programa. Só então era possível fazer algo interessante. E, mesmo assim, esse algo interessante tinha que ser verde, porque o monitor só tinha essa cor.
terça-feira, 8 de dezembro de 2009
Relatórios sobre logs no Oracle
É tão comum usar count() em selects, que é fácil esquecer como o sum() pode ser útil também. Frequentemente, por exemplo, preciso fazer relatórios sobre tabelas de log, totalizando as diferentes ações registradas.
Suponha, por exemplo, que eu tenha uma tabela que registre uploads e downloads de arquivos, por usuários. Esta tabela poderia ter as seguintes colunhas:
Então, para cada linha, cada sum() vai somar 1 ou 0, conforme o valor da coluna acao. O count(1) vai contar o número de linhas agrupadas por usuário e será sempre igual a soma de uploads e downloads, a menos que haja mais tipos de ações.
Suponha, por exemplo, que eu tenha uma tabela que registre uploads e downloads de arquivos, por usuários. Esta tabela poderia ter as seguintes colunhas:
- Nome do usuário;
- Data da ação,
- Tipo da ação (upload ou download).
- O nome do usuário;
- O total de registros;
- O total de uploads;
- O total de downloads.
select usuario,
count(1) total,
sum(case when acao='Upload' then 1 else 0 end) uploads,
sum(case when acao='Download' then 1 else 0 end) downloads
from historico
group by usuario
order by 2 desc
Então, para cada linha, cada sum() vai somar 1 ou 0, conforme o valor da coluna acao. O count(1) vai contar o número de linhas agrupadas por usuário e será sempre igual a soma de uploads e downloads, a menos que haja mais tipos de ações.
segunda-feira, 7 de dezembro de 2009
Usando listas como tabelas em PL/SQL
Um problema recorrente em sistemas web é o de usar listas em consultas. Isto é, o usuário seleciona vários itens de uma lista e isso precisa ser transformado em um SELECT, UPDATE, INSERT, ou mesmo DELETE.
A solução mais comum é a de gerar o SQL dinamicamente. Mas isso cria mais dois problemas: não é seguro (facilita a injeção de SQL) e obriga o Oracle a compilar um novo comando.
É possível passar um array de elementos como parâmetro de uma funcão ou procedure, mas isso não facilita a vida nem do Java (estou presumindo que a interface com o banco seja JDBC) nem do PL/SQL.
Uma solução que surgiu com o Oracle 9i são as funções PIPELINED. Elas permitem escrever uma função cujos resultados podem ser convertidos em linhas de uma tabela.
Vou descrever isso melhor com código. A declaração abaixo é de uma package que contém funções que vamos usar adiante, diretamente em comandos DML:
Temos aqui dois tipos, uma tabela de inteiros e uma tabela de strings. A função IDS() vai transformar um string com inteiros em uma tabela de inteiros. A função NAMES() fará o mesmo por strings.
Então, se eu quiser pesquisar um grupo de pessoas com determinados cargos, posso fazer o seguinte:
A função TABLE transforma os valores de LIST_PKG.NAMES() em uma tabela.
Se eu quiser encontrar um grupo específico de pessoas, posso escrever uma consulta como esta:
É claro que o melhor é colocar essas consultas numa procedure ou function:
E qual a mágica que precisa ocorrer dentro de LIST_PKG? Segue o código:
Como se pode ver, as funções simplesmente dividem strings, separando os elementos entre vírgulas, e depois repassam os valores através do comando PIPE ROW. Pronto, nunca mais será preciso fazer ginástica com strings; essa package ocupa-se de todo o trabalho.
A solução mais comum é a de gerar o SQL dinamicamente. Mas isso cria mais dois problemas: não é seguro (facilita a injeção de SQL) e obriga o Oracle a compilar um novo comando.
É possível passar um array de elementos como parâmetro de uma funcão ou procedure, mas isso não facilita a vida nem do Java (estou presumindo que a interface com o banco seja JDBC) nem do PL/SQL.
Uma solução que surgiu com o Oracle 9i são as funções PIPELINED. Elas permitem escrever uma função cujos resultados podem ser convertidos em linhas de uma tabela.
Vou descrever isso melhor com código. A declaração abaixo é de uma package que contém funções que vamos usar adiante, diretamente em comandos DML:
CREATE OR REPLACE PACKAGE LIST_PKG AS
TYPE ID_TABLE IS TABLE OF INTEGER;
TYPE NAME_TABLE IS TABLE OF VARCHAR2(32767);
FUNCTION IDS(LIST IN VARCHAR2) RETURN ID_TABLE PIPELINED;
FUNCTION NAMES(LIST IN VARCHAR2) RETURN NAME_TABLE PIPELINED;
END;
Temos aqui dois tipos, uma tabela de inteiros e uma tabela de strings. A função IDS() vai transformar um string com inteiros em uma tabela de inteiros. A função NAMES() fará o mesmo por strings.
Então, se eu quiser pesquisar um grupo de pessoas com determinados cargos, posso fazer o seguinte:
SELECT NOME, CARGO, SALARIO
FROM
PESSOAS P
INNER JOIN (TABLE(LIST_PKG.NAMES('Chefão,Chefe,Chefinho')) T
ON T.COLUMN_VALUE=P.CARGO)
A função TABLE transforma os valores de LIST_PKG.NAMES() em uma tabela.
Se eu quiser encontrar um grupo específico de pessoas, posso escrever uma consulta como esta:
SELECT NOME, CARGO, SALARIO
FROM
PESSOAS P
INNER JOIN (TABLE(LIST_PKG.IDS('34,45,67,1,99')) T
ON T.COLUMN_VALUE=P.MATRICULA)
É claro que o melhor é colocar essas consultas numa procedure ou function:
PROCEDURE BUSCAR_PESSOAS_PROC (
P_MATRICULAS IN VARCHAR2,
P_CURSOR OUT SYS_REFCURSOR
) IS
BEGIN
OPEN P_CURSOR FOR
SELECT NOME, CARGO, SALARIO
FROM
PESSOAS P
INNER JOIN (TABLE(LIST_PKG.IDS(P_MATRICULAS)) T
ON T.COLUMN_VALUE=P.MATRICULA);
END;
E qual a mágica que precisa ocorrer dentro de LIST_PKG? Segue o código:
CREATE OR REPLACE PACKAGE BODY LIST_PKG AS
FUNCTION IDS(LIST IN VARCHAR2) RETURN ID_TABLE PIPELINED IS
LEN PLS_INTEGER;
TAB SYS.DBMS_UTILITY.UNCL_ARRAY;
BEGIN
IF LIST IS NULL THEN
RETURN;
END IF;
SYS.DBMS_UTILITY.COMMA_TO_TABLE(
'"' || REPLACE(LIST, ',', '","') || '"',
LEN, TAB);
FOR I IN 1 .. LEN LOOP
PIPE ROW(TRANSLATE(TAB(I), 'A"', 'A'));
END LOOP;
RETURN;
END IDS;
FUNCTION NAMES(LIST IN VARCHAR2) RETURN NAME_TABLE PIPELINED IS
LEN PLS_INTEGER;
TAB SYS.DBMS_UTILITY.UNCL_ARRAY;
BEGIN
IF LIST IS NULL THEN
RETURN;
END IF;
SYS.DBMS_UTILITY.COMMA_TO_TABLE(
'"' || REPLACE(LIST, ',', '","') || '"',
LEN, TAB);
FOR I IN 1 .. LEN LOOP
PIPE ROW(TRANSLATE(TAB(I), 'A"', 'A'));
END LOOP;
RETURN;
END NAMES;
END LIST_PKG;
Como se pode ver, as funções simplesmente dividem strings, separando os elementos entre vírgulas, e depois repassam os valores através do comando PIPE ROW. Pronto, nunca mais será preciso fazer ginástica com strings; essa package ocupa-se de todo o trabalho.
sábado, 5 de dezembro de 2009
A máquina do tempo
Há 25 anos, em 1985, um homem foi a uma loja chamada Watford Electronics, na Inglaterra, e comprou um micro chamado BBC, fabricado pela Acorn. Este micro foi produzido para fins educacionais, portanto era bastante robusto. Dizem que é o único micro que sobrevive a cair numa escada, já que crianças inglesas são uns pequenos hooligans desde cedo. Era perfeito para a longa missão que estava sendo iniciada.
Este homem ligou o micro e o deixou rodando sem parar até hoje. Ele queria fazer uma conta realmente cabeluda. Rodando a 2MHz, as coisas demoram a acontecer.
Ontem, finalmente, o micro terminou. Deu erro.
Mas o homem acha melhor repetir o cálculo no Pentium de 2GHz que o filho dele comprou na segunda-feira e que já terminou a mesma conta.
Se ele descobrir outra conta cabeluda (vai ter que ser umas 2 mil vezes mais cabeluda que a primeira) para fazer, será que vai ter paciência para esperar 25 anos antes de começar a calcular?
Este homem ligou o micro e o deixou rodando sem parar até hoje. Ele queria fazer uma conta realmente cabeluda. Rodando a 2MHz, as coisas demoram a acontecer.
Ontem, finalmente, o micro terminou. Deu erro.
Mas o homem acha melhor repetir o cálculo no Pentium de 2GHz que o filho dele comprou na segunda-feira e que já terminou a mesma conta.
Se ele descobrir outra conta cabeluda (vai ter que ser umas 2 mil vezes mais cabeluda que a primeira) para fazer, será que vai ter paciência para esperar 25 anos antes de começar a calcular?
sexta-feira, 4 de dezembro de 2009
Pesquisar em ZIPs sem abri-los!
Há pouco tempo fui incumbido de encontrar dentre uma centena de arquivos zips, aqueles que tinham um pequeno defeito. Os zips continham muitos arquivos pequenos e um xml que descrevia o conteúdo; o defeito estava nesse xml.
Parecia uma tarefa perfeita para um script em Perl. Achei então o módulo Archive::Zip, que tornou a tarefa muito mais simples do que eu antecipara.
O código abaixo percorre todos os arquivos de uma pasta, abre cada um deles e analisa o xml.
Parecia uma tarefa perfeita para um script em Perl. Achei então o módulo Archive::Zip, que tornou a tarefa muito mais simples do que eu antecipara.
O código abaixo percorre todos os arquivos de uma pasta, abre cada um deles e analisa o xml.
O conteúdo inteiro de cada arquivo vai para a variável $data e com ela podemos analisar o xml para decidir o que fazer, como se faz com um string qualquer.
use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
my $dirname='/tmp/zips';
opendir(DIR, $dirname) or die "Can't open dir\n";
while (defined($file = readdir(DIR))) {
#evita tentar abrir "." e ".."
if(length($file)>2) {
my $zip=Archive::Zip->new();
#abre zip
$zip->read("$dirname/$file");
#lê arquivo
my $data=$zip->contents("index.xml");
#processar arquivo aqui
}
}
O monitor está obsoleto
Este ano comprei uma TV de LCD de 32". Como ela é de alta resolução, resolvi logo ligar o computador nela para jogar TrackMania. Percebi, então, que os monitores estão obsoletos. Não há mais razão para ter uma TV e um monitor em casa. Posso economizar o espaço.
A tecnologia deu uma volta completa e voltamos aos anos 1980, quando os micros de 8 bits eram ligados à TV. Poucos eram os sortudos a terem um monitor. A resolução era ruim, mas dada a pouca memória disponível, não se perdia muito. De qualquer maneira, os monitores não agregavam resolução, na maior parte dos casos. Eles apenas tinham uma imagem mais nítida, o que convinha para quem precisava trabalhar com eles. Para jogos, não tinha sentido o custo extra.
Pois, agora as TVs têm uma resolução maior que a maior parte dos monitores. Está faltando apenas que voltem os micros com CPU e teclado numa só peça. Imagine um netbook sem LCD e custando US$100.
É o ZX Spectrum de 2010!
A tecnologia deu uma volta completa e voltamos aos anos 1980, quando os micros de 8 bits eram ligados à TV. Poucos eram os sortudos a terem um monitor. A resolução era ruim, mas dada a pouca memória disponível, não se perdia muito. De qualquer maneira, os monitores não agregavam resolução, na maior parte dos casos. Eles apenas tinham uma imagem mais nítida, o que convinha para quem precisava trabalhar com eles. Para jogos, não tinha sentido o custo extra.
Pois, agora as TVs têm uma resolução maior que a maior parte dos monitores. Está faltando apenas que voltem os micros com CPU e teclado numa só peça. Imagine um netbook sem LCD e custando US$100.
É o ZX Spectrum de 2010!
quinta-feira, 3 de dezembro de 2009
Leitura: The Newtonian Casino
The Newtonian Casino
Thomas A. Bass
Peguin Science
ISBN 0-14-014594
No fim dos anos 70, um grupo de alunos de física reuniu-se para tentar ganhar fortuna nos cassinos. Há pouco tempo tinham surgido os primeiros microprocessadores e, com o 6502, eles estavam ficando mais baratos. Estudando a roleta, concluiram que é possível prever onde a bola vai cair, com precisão suficiente para virar as probablidades a seu favor.
Eles construiram um computador que cabia num sapato. Em realidade, como não podiam colocar todo o computador e os elementos de entrada num único aparelho, dividiram a máquina em dois. Uma parte era usada para a entrada de dados; com os dedos do pé, o jogador informava ao computador as condições iniciais do jogo. Clicando em determinados pontos da trajetória da bola, a máquina coletava informações suficientes para prever em qual quadrante o jogo terminaria. Os dados eram transmitidos para a segunda máquina por rádio. Esta, por sua vez, calculava o resultado final e o informava ao segundo jogador através de atuadores presos ao tórax.
As roletas também têm a propriedade interessante de que os números dum mesmo quadrante não ficam dispostos lado a lado na mesa. Apenas uma pessoa foi observadora o suficiente para perceber essa tática.
Os estudantes precisaram demonstrar grandes conhecimentos em:
- Probabilidade e estatística;
- Circuitos;
- Programação.
Além disso, precisaram treinar arduamente para conseguir os reflexos necessários para entrar os dados precisamente com os dedos do pé. Tinham também que ler o resultado através de pulsos na pele e rapidamente espalhar as fichas sobre a mesa.
Em resumo, foi um conjunto extraordinário de habilidades reunidas em apenas meia-dúzia de estudantes.
Como os cassinos não tinham idéia de que fosse possível prever o resultado da roleta, nem que seria possível colocar um computador num sapato (em 1979), mesmo quando um guarda encontrou um dos estudantes no banheiro consertando uma máquina, eles não foram descobertos ("Estou apenas consertando meu rádio"!). Mesmo assim, depois de anos dedicados ao projeto, eles não chegaram a ter o sucesso que esperavam e desistiram.
Este livro oferece uma mistura interessante de aventura e proeza técnica. Eu esperava mais detalhes técnicos do computador, mas também me surpreendi com algumas questões que não havia antecipado. Há, por exemplo, um gráfico com as probabilidades de ruina do jogador, dada sua vantagem (ou desvantagem) sobre o jogo. Isto é, mesmo com as probabilidades a seu favor, o jogador corre o risco de perder todo o seu dinheiro se tiver uma seqüência suficientemente longa de derrotas e isso determina a quantidade mínima de dinheiro para iniciar um jogo. Além disso, é preciso ter uma estratégia para despistar os cassinos, que estão constantemente vigilantes.
Poderia haver uma versão mais técnica desse livro. Mesmo assim, eu o recomendo simplesmente porque descreve o evento extraordinário que foi a união de pessoas talentosas numa aventura, no preciso momento da história em que se tornou possível colocar um computador dentro de um sapato.
terça-feira, 1 de dezembro de 2009
Drives novos em micros antigos (antigos mesmo!)
Havia uma época em que um drive de disquete era um luxo. Quando eu falo para meus colegas mais novos sobre gravar programas em fita, eles costumam pensar em DAT ou Exabyte; custam a acreditar que usavam-se fitas cassete mesmo.
Tenho umas revistas antigas guardadas e numa delas, de 1986, um drive de 5.25" e densidade simples está anunciado por 75 libras. Pela cotação de hoje, seriam R$215. Mas eu achei uma calculadora de inflação que calculou que as 75 libras hoje equivaleriam a 165, que, por sua vez, equivalem a R$472. Pois, fui a uma loja de informática, das mais caras, e comprei um drive de 3.5" e alta densidade por R$15. Não concebo como um aparelho razoavelmente complexo consiga ser fabricado na China, enviado para o outro lado do mundo e vendido num shopping por R$15. Maravilhas da globalização.
Pois bem, uma vez comprado o aparelho procedi a conectá-lo a um micro pré-histórico: um BBC B+ 128KB. Este micro já foi chamado de Rolls-Royce dos microcomputadores. Eu também gosto dele e por isso o guardo até hoje. Ele foi comprado em 1985. Há quase 25 anos, portanto.
Ele usa uma controladora Western Digital 1770 (WD1770 para os íntimos), que foi muita usada em micros de 8 e 16 bits. A WD1700 suporta densidade simples e dupla, mas o BBC veio equipado com o DFS (Disc Filing System), que suporta apenas densidade simples. Em densidade simples, cada setor comporta 256 bytes. O DFS permite 40 ou 80 trilhas de 10 setores cada. Usando os dois lados de um disquete (e não eram todos que podiam!), era possível gravar impressionantes 400KB numa única mídia. Era mais do que um PC alcançava; mesmo com densidade dupla, os PCs usavam apenas 40 trilhas de 9 setores, resultando em 360KB por disquete.
De qualquer forma, os drives atuais têm uma característica importante: assim como os órgãos vestigiais que o homem mantém para lembrar-se do seu passado, eles gravam em densidades simples (SD), dupla (DD) e alta (HD). E qual a diferença? Entre a densidade simples e a dupla, a única é a codificação. A primeira usa FM (Frequency Modulation) e a segunda usa MFM (Modified Frequency Modulation). A controladora envia os dados já codificados para o drive e este nem toma conhecimento da diferença. Com alta densidade, no entanto, a trama engrossa. Os disquetes de alta densidade possuem um furo extra (logo, possuem dois - um para proteção e outro para indicar a densidade alta) e características físicas um pouco diferentes. Por causa da densidade maior, o drive precisa gravar os dados com uma coercitividade maior. Densidade simples e dupla são gravadas a 600oe (oersteds) e a densidade alta é gravada a 720oe.
Existiu também uma densidade quadrupla (QD) em drives fabricados pela DEC. Eles permitiam gravar 2.88MB num disquete, a 900oe e com um furo adicional (três ao todo, portanto). Só vi uma vez um drive desses.
Usando um cabo plano (flat) normal de PC (de 34 vias) conectei o drive ao micro. Ele só funcionou no conector do meio. O PC chama o drive do meio do cabo de B e o da ponta de A. O BBC trata o do meio como o primeiro drive e distingue as cabeças de leitura, sendo a de baixo a 1 e a superior a 3. O drive da ponta é o primeiro, com as cabeças 0 e 2. Quando o sistema de disco tem que caber num ROM de 16KB, algumas esquisitices acabam aparecendo.
Pois bem, após muitas investigações descobri que o BBC usa o cabeamento Shugart e o PC usa o Shugart com leves modificações. O Shugart permite até 3 drives. O pino 16 indica quem um drive pode girar e os pinos 10, 12 e 14 indicam qual drive. No PC, para simplificar o hardware, o pino 10 envia a ordem de ligar e o pino 14 indica que a ordem é para drive 2 (A). Os pinos 12 e 16 fazem o mesmo pelo drive 1 (B).
O que eu precisava então, era do cabo certo. Fui a uma loja de eletrônica e tive a grata surpresa de descobrir que os cabos planos são amplamente usados na eletrônica. Eles não foram projetados apenas para ligar micros a drives. Existem cabos planos de diversos tipos e números de pinos. Basta comprar um metro de cabo, três terminadores e trocar o pino 10 pelo 12.
Os cabos de PC também têm uma voltinha. Essa voltinha existe porque os drives são todos fabricados como drive B. Para economizar alguns centavos na montagem dos PCs, as fábricas passaram a deixar os drives sempre na mesma configuração; o cabo engana o computador a tratar o drive da ponta como o A. A tal voltinha abrange todos os pinos do 10 ao 16, mas os pinos 13 e 14 apenas carregam o terra. Com o tempo, o jumper que alterava a configuração sumiu da maioria dos drives.
Tive medo de que as mídias de alta densidade não funcionassem bem com densidade simples. Ainda é possível conseguir mídias de densidade dupla, mas são mais caras e não estão disponíveis em qualquer loja. Há algumas lojas na Alemanha que ainda vendem esses disquetes; é herança do Amiga, que foi muito popular por lá. Tapando o segundo furo de uma mídia HD, enganei o drive a tratá-la como uma mídia SD/DD. Gravei um "Hello World" em BASIC e, por sorte, consegui ler de volta o programa.
Os fabricantes mantiveram o mesmo padrão quando evoluiram dos drives de 8" para os de 5.25" e finalmente para os de 3.5". Se alguém tiver um micro realmente antigo com um drive de 8", provavelmente conseguirá conectar um drive de 3.5" novo, sem grandes dificuldades.
Tenho umas revistas antigas guardadas e numa delas, de 1986, um drive de 5.25" e densidade simples está anunciado por 75 libras. Pela cotação de hoje, seriam R$215. Mas eu achei uma calculadora de inflação que calculou que as 75 libras hoje equivaleriam a 165, que, por sua vez, equivalem a R$472. Pois, fui a uma loja de informática, das mais caras, e comprei um drive de 3.5" e alta densidade por R$15. Não concebo como um aparelho razoavelmente complexo consiga ser fabricado na China, enviado para o outro lado do mundo e vendido num shopping por R$15. Maravilhas da globalização.
Pois bem, uma vez comprado o aparelho procedi a conectá-lo a um micro pré-histórico: um BBC B+ 128KB. Este micro já foi chamado de Rolls-Royce dos microcomputadores. Eu também gosto dele e por isso o guardo até hoje. Ele foi comprado em 1985. Há quase 25 anos, portanto.
Ele usa uma controladora Western Digital 1770 (WD1770 para os íntimos), que foi muita usada em micros de 8 e 16 bits. A WD1700 suporta densidade simples e dupla, mas o BBC veio equipado com o DFS (Disc Filing System), que suporta apenas densidade simples. Em densidade simples, cada setor comporta 256 bytes. O DFS permite 40 ou 80 trilhas de 10 setores cada. Usando os dois lados de um disquete (e não eram todos que podiam!), era possível gravar impressionantes 400KB numa única mídia. Era mais do que um PC alcançava; mesmo com densidade dupla, os PCs usavam apenas 40 trilhas de 9 setores, resultando em 360KB por disquete.
De qualquer forma, os drives atuais têm uma característica importante: assim como os órgãos vestigiais que o homem mantém para lembrar-se do seu passado, eles gravam em densidades simples (SD), dupla (DD) e alta (HD). E qual a diferença? Entre a densidade simples e a dupla, a única é a codificação. A primeira usa FM (Frequency Modulation) e a segunda usa MFM (Modified Frequency Modulation). A controladora envia os dados já codificados para o drive e este nem toma conhecimento da diferença. Com alta densidade, no entanto, a trama engrossa. Os disquetes de alta densidade possuem um furo extra (logo, possuem dois - um para proteção e outro para indicar a densidade alta) e características físicas um pouco diferentes. Por causa da densidade maior, o drive precisa gravar os dados com uma coercitividade maior. Densidade simples e dupla são gravadas a 600oe (oersteds) e a densidade alta é gravada a 720oe.
Existiu também uma densidade quadrupla (QD) em drives fabricados pela DEC. Eles permitiam gravar 2.88MB num disquete, a 900oe e com um furo adicional (três ao todo, portanto). Só vi uma vez um drive desses.
Usando um cabo plano (flat) normal de PC (de 34 vias) conectei o drive ao micro. Ele só funcionou no conector do meio. O PC chama o drive do meio do cabo de B e o da ponta de A. O BBC trata o do meio como o primeiro drive e distingue as cabeças de leitura, sendo a de baixo a 1 e a superior a 3. O drive da ponta é o primeiro, com as cabeças 0 e 2. Quando o sistema de disco tem que caber num ROM de 16KB, algumas esquisitices acabam aparecendo.
Pois bem, após muitas investigações descobri que o BBC usa o cabeamento Shugart e o PC usa o Shugart com leves modificações. O Shugart permite até 3 drives. O pino 16 indica quem um drive pode girar e os pinos 10, 12 e 14 indicam qual drive. No PC, para simplificar o hardware, o pino 10 envia a ordem de ligar e o pino 14 indica que a ordem é para drive 2 (A). Os pinos 12 e 16 fazem o mesmo pelo drive 1 (B).
O que eu precisava então, era do cabo certo. Fui a uma loja de eletrônica e tive a grata surpresa de descobrir que os cabos planos são amplamente usados na eletrônica. Eles não foram projetados apenas para ligar micros a drives. Existem cabos planos de diversos tipos e números de pinos. Basta comprar um metro de cabo, três terminadores e trocar o pino 10 pelo 12.
Os cabos de PC também têm uma voltinha. Essa voltinha existe porque os drives são todos fabricados como drive B. Para economizar alguns centavos na montagem dos PCs, as fábricas passaram a deixar os drives sempre na mesma configuração; o cabo engana o computador a tratar o drive da ponta como o A. A tal voltinha abrange todos os pinos do 10 ao 16, mas os pinos 13 e 14 apenas carregam o terra. Com o tempo, o jumper que alterava a configuração sumiu da maioria dos drives.
Tive medo de que as mídias de alta densidade não funcionassem bem com densidade simples. Ainda é possível conseguir mídias de densidade dupla, mas são mais caras e não estão disponíveis em qualquer loja. Há algumas lojas na Alemanha que ainda vendem esses disquetes; é herança do Amiga, que foi muito popular por lá. Tapando o segundo furo de uma mídia HD, enganei o drive a tratá-la como uma mídia SD/DD. Gravei um "Hello World" em BASIC e, por sorte, consegui ler de volta o programa.
Os fabricantes mantiveram o mesmo padrão quando evoluiram dos drives de 8" para os de 5.25" e finalmente para os de 3.5". Se alguém tiver um micro realmente antigo com um drive de 8", provavelmente conseguirá conectar um drive de 3.5" novo, sem grandes dificuldades.
Paginação de resultados no Oracle
Certa vez deparei-me com um problema aparentemente simples: era preciso gerar um relatório paginado que, ademais, exibisse a cada página o primeiro e o último registros. O sistema era escrito em Java e a interface em JSP. Eu gosto de separar bem as atribuições de cada camada, então não estava disposto a fazer no Java nada além de simplesmente percorrer o ResultSet e exibir os dados.
Após algumas tentativas criei uma consulta com a estrutura da seguinte:
Nesse caso, a consulta retorna uma lista de usuários (cujos nomes iniciem com B) com seus respectivos CPFs e ordenados por nome. A cláusula where indica que são pesquisados todos os registros entre o décimo e o vigésimo, além do primeiro e o último. O select da consulta é o mais de dentro, os outros apenas fazem uma pequena ginástica para paginar o resultado.
Como a consulta retorna o primeiro e o último registros, é possível colocar no cabeçalho uma indicação como "Registros 10 a 20 (Beatriz a Bianca) de 81 (Bárbara a Bruna)", sem que seja preciso executar nenhuma consulta adicional.
Não é difícil criar uma classe para automaticamente envolver qualquer consulta com os selects externos, mas o melhor mesmo é colocar as consultas dentro de uma procedure. Esse tipo de select, eu descobri, é igual ou melhor a simplesmente contar os registros e depois selecionar os interessantes. Se não está claro o que ele faz, eis um passo-a-passo:
Essa ginástica toda acontece porque o Oracle atribui o rownum aos registros e depois os ordena. Se não fosse por isso, bastariam dois selects. Apesar de contente com a solução, eu achava que ainda era possível simplificar. Descobri então, as funções analíticas. Não vou explicar o que fazem; vou pular direto para a solução.
A função row_number() produz um número para cada registro, conforme a ordenação indicada pela cláusula over. O select interno ordena os registros ao contrário, mas o row_number() over (order by nome asc) os conta na ordem direta. O select externo inverte a seleção e agrega um rownum. Como o rownum é atribuido antes da ordenação, o registro com rb=1 é o último (porque é o primeiro registro do select interno). A coluna ra já está na ordem direta, então podemos usá-la para selecionar o primeiro registro e os da página atual (entre 10 e 20).
Após algumas tentativas criei uma consulta com a estrutura da seguinte:
select ub.*, rownum rb from ( select ua.*, rownum ra from ( select nome, cpf from usuario where nome like 'B%' order by nome asc ) ua order by nome desc ) ub where ra=1 or rownum=1 or ra between 10 and 20 order by nome asc
Nesse caso, a consulta retorna uma lista de usuários (cujos nomes iniciem com B) com seus respectivos CPFs e ordenados por nome. A cláusula where indica que são pesquisados todos os registros entre o décimo e o vigésimo, além do primeiro e o último. O select da consulta é o mais de dentro, os outros apenas fazem uma pequena ginástica para paginar o resultado.
Como a consulta retorna o primeiro e o último registros, é possível colocar no cabeçalho uma indicação como "Registros 10 a 20 (Beatriz a Bianca) de 81 (Bárbara a Bruna)", sem que seja preciso executar nenhuma consulta adicional.
Não é difícil criar uma classe para automaticamente envolver qualquer consulta com os selects externos, mas o melhor mesmo é colocar as consultas dentro de uma procedure. Esse tipo de select, eu descobri, é igual ou melhor a simplesmente contar os registros e depois selecionar os interessantes. Se não está claro o que ele faz, eis um passo-a-passo:
- O select interno realiza a consulta;
- O select do meio inverte a seleção e agrega o rownum (ra tem a numeração na ordem correta, embora os registros fiquem na ordem invertida);
- O select externo inverte novamente a seleção e filtra os registros interessantes.
Essa ginástica toda acontece porque o Oracle atribui o rownum aos registros e depois os ordena. Se não fosse por isso, bastariam dois selects. Apesar de contente com a solução, eu achava que ainda era possível simplificar. Descobri então, as funções analíticas. Não vou explicar o que fazem; vou pular direto para a solução.
select u.*, rownum rb from ( select nome, cpf, row_number() over (order by nome asc) ra from usuario where nome like 'B%' order by nome desc ) u where ra=1 or rownum=1 or ra between 10 and 20 order by nome asc
A função row_number() produz um número para cada registro, conforme a ordenação indicada pela cláusula over. O select interno ordena os registros ao contrário, mas o row_number() over (order by nome asc) os conta na ordem direta. O select externo inverte a seleção e agrega um rownum. Como o rownum é atribuido antes da ordenação, o registro com rb=1 é o último (porque é o primeiro registro do select interno). A coluna ra já está na ordem direta, então podemos usá-la para selecionar o primeiro registro e os da página atual (entre 10 e 20).
Leitura: Racing The Beam
Racing the Beam - The Atari Video Computer System
Nick Montfort and Ian Bogost
The MIT Press
ISBN-10:0-262-01257-X
ISBN-13:978-0-262-01257-7
A computação gráfica e a programação de jogos são excelentes escolas de programação. É necessário utilizar estruturas de dados complexas, sendo preciso estar sempre procurando um equilíbrio entre a complexidade das estruturas, do código necessário para percorrê-las e o espaço que utilizam. Além disso, há uma luta constante contra os limites do hardware.
Por isso, comecei a ler Racing The Beam com a expectativa de encontrar técnicas de programação, no mínimo, interessantes. O que eu achei foi deveras surpreendente. A minha expectativa inicial era a de encontrar alguma justificativa para a má-qualidade dos jogos do 2600 e acabei descobrindo que o aparelho é extremamente difícil de programar. Os criadores de jogos eram pessoas realmente dedicadas e habilidosas! A verdade é que ainda são, porque há quem escreva jogos para o 2600 ainda hoje.
No cerne do 2600 havia um processador 6507, que é um 6502 com apenas 13 linhas de endereçamento (contra as 16 originais). Com isso, o processador poderia endereçar apenas 8KB. No entanto, a interface dos cartuchos tinha ainda uma linha a menos; os jogos estavam limitados, portanto, a 4KB. Eu já começava a suspeitar que a programação seria difícil: onde seria possível guardar os bits da tela? Com 160x192 pixeis e 4 cores por linha (sendo possíveis 128 cores numa tela), seriam precisos quase 8KB para uma única tela de jogo. A tática comum de usar double-buffering (duas áreas de desenho) para evitar flickering (quando a animação pisca porque a atualização da tela não está sincronizada com a exibição) estava fora de questão desde o início. Os 1,19MHz do processador não eram uma restrição tão séria quanto a de que a máquina tinha apenas 128 bytes de RAM. E esses 128 bytes tinham que servir para as variáveis e a pilha.
A explicação para onde armazenar os gráficos está no título de livro: é preciso correr com o feixe da TV. Isto é, a cada linha, o programa deve interagir com o chip de geração de sinal de vídeo (TIA - Television Interface Adapter) para gerar os gráficos. Os gráficos simplesmente não eram armazenados em RAM, eles eram exibidos proceduralmente.
O programador tinha dois momentos para respirar: entre linhas e ao fim de uma tela. Ao desenhar uma linha, o feixe volta ao início e desce uma posição, para desenhar a próxima. E ao chegar ao fim da tela, o feixe volta para o início. O tempo que o feixe leva para voltar ao início da tela é o maior tempo que o programa tem disponível para processar as ações dos jogadores e calcular o próximo passo do jogo. Com uma velocidade de 1,19Mhz, o processador poderia, hipoteticamente, executar mais de um milhão de instruções. No entanto, a maior parte das instruções do 6507 precisa de mais de um ciclo. Muitas levam 2 ciclos e as mais complexas ocupam até 7 ciclos. As instruções ocupam de um byte até três. Apenas as mais simples, como INX (INcrement X register), ocupam apenas um byte e levam um ciclo para serem executadas. Para desenhar uma linha, havia apenas 76 ciclos do processador. Para processar as ações dos jogadores e calcular o próximo passo do jogo, 5320 ciclos. Dificilmente seria possível encaixar mais que 2000 instruções entre quadros. De qualquer forma, como o jogo estava limitado a 4KB, nem havia espaço para guardar mais instruções.
Um segundo chip, o PIA (Peripheral Interface Adapter) oferecia 3 funções: um temporizador programável, memória (os 128 bytes) e duas portas de I/O. O circuito, portanto, era muito simples. Não havia muito dentro de um 2600.
O hardware não é inteiramente malévolo e oferece uma facilidade: o programador pode usar sprites. São possíveis dois sprites de um pixel para mísseis, um sprite de um pixel para um tiro, dois sprites para jogadores e um sprite para a área de jogo. Em realidade, parece que o hardware foi desenhado para um tipo bem específico de jogo. E a verdade é justamente essa: ao projetar o hardware, um conjunto bem específico de características de jogos foi considerada. As escolhas de projeto foram influenciadas pelo jogo Pong, que existiu primeiro em bares e mais tarde foi lançado pela Atari como um jogo doméstico, para conectar na TV. O sucesso dele foi tão grande, que a Atari projetou o 2600 como a máquina mais barata possível que pudesse executar jogos do tipo Pong. O resultado, no entanto, permitiu uma gama interessante de jogos.
O livro analisa alguns jogos considerados importantes por motivos técnicos: Combat, Adventure, Pac-Man, Yar's Revenge, Pitfall! e Starwars (The Empire Strikes Back). Além disso, os autores preocuparam-se em analisar o ecosistema da plataforma e como os programadores foram compelidos a testar os limites do hardware, quando máquinas mais potentes começaram a aparecer. Surgiram maneiras de colocar mais código dentro dos cartuchos, por exemplo. Usando paginação, alguns cartuchos alcançaram 16KB.
Hoje em dia, já temos computadores que podem executar bilhões de instruções entre quadros e placas de vídeo ainda mais poderosas, porque executam muitas instruções em paralelo. O hardware já não é penoso para os programadores e o jogos são desenvolvidos em equipes. Tornou-se caro e trabalhoso porque a exigência dos compradores é muito maior. Para o 2600, os jogos eram escritos por um ou dois programadores extremamente hábeis. Era preciso conhecer o hardware intimamente e ser criativo o suficiente para escrever um jogo interessante sobre pouquíssimos recursos. Por isso, recomendo este livro para quem aprecia não apenas a história dos jogos, mas também a verdadeira habilildade em programação.
Assinar:
Postagens (Atom)