segunda-feira, 31 de maio de 2010

O oráculo e o camelo

Tive que carregar uma base de dados com o conteúdo de dois DVDs cheios de informações. Os dados estavam separados em arquivos texto que por sua vez estavam dentro vários níveis de pastas.

A primeira tentativa foi usar um script em Perl. A solução funcionou, mas não foi rápida o suficiente para o curto espaço de tempo disponível (como sempre, o prazo era exíguo). Rápido mesmo foi usar o SQL Loader.

Mesmo assim, vou descrever a solução em Perl. Ela não é tão rápida, mas ela é mais flexível.

Em primeiro lugar, usei o módulo Find para buscar os arquivos. O módulo é dos básicos, então sempre está disponível.

Uma linha basta para disparar uma busca em uma hierarquia de pastas:

find(\&process_file, $ARGV[0]);

Essa linha dispara a busca com o valor do primeiro argumento dado ao script. Esse argumento deve conter o caminho de uma pasta. Para cada arquivo que o find() encontrar, ele invocará a função process_file().

Antes de apresentar a process_file(), vou mostrar a estrutura de dados que faz o relacionamento entre os nomes dos arquivos, seus formatos e os respectivos inserts.

my $tables={
'PRODUTOS' => {
format => 'A4A20A10',
insert => q{insert into produtos (codigo, descricao, valor)
values (?,?,?/100)}
},
'PESSOAS' => {
format => 'A11A60A8',
insert => q{insert into pessoas (cpf, nome, data_nascimento)
values (?,?,to_date(?, 'DDMMYYYY'))}
}
};

A referência $table aponta para um hash; cada chave do hash representa uma tabela cujo nome coincide com o nome do arquivo; associado a cada nome de tabela está um formato e um insert. Uso referências como poderia usar hashes diretamente.

A função process_file(), então, determina se existe um mapeamento para cada arquivo encontrado e invoca a função load() quando existir.

sub process_file {
my $filename=$File::Find::name;
my $name=$_;

if($name=~/(\w+)\.txt/) {
if(exists $tables->{$1}) {
load($filename, $1);
}
}
}

Os nomes dos arquivos têm os mesmos nomes das tabelas e o sufixo "txt". Usando uma expressão regular, determino se o arquivo tem o sufixo desejado e uso a primeira parte do nome ($1) para encontrar o mapeamento.

Para extrair os dados dos arquivos, uso a função unpack(). Esta função recebe um string que descreve uma linha e a linha propriamente dita. Usando a descrição, ela divide a linha e produz um array com os campos. Então, para percorrer um arquivo texto com 3 colunas de 10 caracteres cada, bastaria fazer o seguinte:

while(<FILE>) {
my @cols=unpack("A10A10A10");
}

O segundo parâmetro é implícito (<FILE> coloca a linha lida em $_ e unpack() o usa como parâmetro implícito).

Para gravar os dados, uso o DBI, que é o módulo de acesso a bases de dados. O exemplo abaixo mostra como abrir uma conexão, preparar um comando e executá-lo.

$db = DBI->connect( "dbi:Oracle:host=HOST;sid=SID", "SCHEMA", "PASSSWD" )
|| die( $DBI::errstr . "\n" );
$st=$db->prepare('INSERT INTO PRODUTOS (CODIGO, NOME, VALOR) VALUES(?,?,?)');
$st->execute('0123', 'BOLA', 49.95);

Juntando o unpack() com o DBI, o resultado é:

sub load {
my ($filename, $table)=@_;
my $format=$tables->{$table}->{format};
my $insert=$tables->{$table}->{insert};

open(my $fh, "<$filename");
my $st=$db->prepare($insert);
while(<$fh>) {
my @row=unpack($format, $_);
$st->execute(@row);
}
$db->commit();
close($fh);
}

A função unpack() produz um array e o método execute() recebe um array. Não podia ser mais fácil. Executo um commit() por arquivo, mas poderia muito bem executar um único commit no fim de tudo.

E, finalmente, juntando todas as partes tem-se:

#!/usr/bin/perl
use DBI;
use File::Find;

my $db = DBI->connect( "dbi:Oracle:host=HOST;sid=SID", "SCHEMA", "PASSSWD" )
|| die( $DBI::errstr . "\n" );
$db->{AutoCommit} = 0;
$db->{RaiseError} = 1;

my $tables={
'PRODUTOS' => {
format => 'A4A20A10',
insert => q{insert into produtos (codigo, descricao, valor)
values (?,?,?/100)}
},
'PESSOAS' => {
format => 'A11A60A8',
insert => q{insert into pessoas (cpf, nome, data_nascimento)
values (?,?,to_date(?, 'DDMMYYYY'))}
}
};

sub load {
my ($filename, $table)=@_;
my $format=$tables->{$table}->{format};
my $insert=$tables->{$table}->{insert};

open(my $fh, "<$filename");
my $st=$db->prepare($insert);
while(<$fh>) {
my @row=unpack($format, $_);
$st->execute(@row);
}
$db->commit();
close($fh);
}

sub process_file {
my $filename=$File::Find::name;
my $name=$_;

if($name=~/(\w+)\.txt/) {
if(exists $tables->{$1}) {
load($filename, $1);
}
}
}

find(\&process_file, $ARGV[0]);

sábado, 22 de maio de 2010

Império dos 8 bits II

A Europa parece ter sido o centro do Império de 8 bits porque a maior parte de seus países fabricou máquinas desse tipo. Um fato interessante é que todos os países socialistas tiveram suas próprias máquinas, exceto a Albânia.


No ocidente, há algumas faltas surpreendentes, como a Suíça e a Noruega. Ambos são países ricos e tecnologicamente avançados. Mesmo assim, não parecem ter se interessado pelos micros.

Portugal, Grécia e Irlanda estão hoje no centro dos problemas financeiros da Europa e na década de 1980 eram os mais pobres do ocidente. Não é estranho que não tenham tido seus próprios computadores.

O Reino Unido era o centro mais ativo. No auge, teve 600 fabricantes. Pode-se contar nos dedos os modelos dos outros países. A França foi a única a fazer frente aos ingleses, talvez por rivalidade. Enquanto os ingleses atacavam em todas as frentes (e ganhavam muito dinheiro com jogos), os sisudos franceses pareciam estar mais interessados no mercado corporativo.

Os países do leste fabricaram muitos clones do Apple II e do ZX Spectrum, embora alguns modelos tenham sido realmente interessantes. Os Pravetz foram muito usados em colégios da Bulgária e neles foi desenvolvido o vírus Dark Avenger, que atacava os Apple II. O Jet da Romênia (fabricados pela Electromagnetica) usava a carcaça de um telefone e as teclas eram identificadas com papéis escritos à mão. Os alemães orientais levaram muito a sério a concorrência com o ocidente e chegaram a produzir memórias de 1 megabit.

Os russos usavam os micros principalmente nos centros de pesquisa, mas em 1983 publicaram o projeto do Micro-80 na revista Radio. Foi o primeiro computador DIY e baseava-se no processador 8080 da Intel. Seguiram-no vários projetos publicados em revistas soviéticas.

Um pouco mais atrasados, os países do leste continuaram a produzir micros de 8 bits até o início dos anos 1990. Foi a queda do muro de Berlim que decretou o fim dessa tecnologia na Europa.

quarta-feira, 19 de maio de 2010

Inflação operacional

Meu micro estava há um bom tempo teimando em não desligar completamente. Ele até desligava os discos, mas continuava com a ventilação funcionando. Como última tentativa, resolvi atualizar a BIOS. Funcionou, mas um detalhe importante apareceu.

A imagem da BIOS tem 512KB. Isso é exatamente o tamanho da ROM que continha o sistema operacional RISC OS do Archimedes. Este era um sistema operacional multi-tarefa com interface gráfica, conforme ilustra a figura abaixo.


Então, o que há 20 anos seria o suficiente para um sistema operacional bastante avançado, hoje mal serve para ligar e deligar a máquina.

terça-feira, 18 de maio de 2010

Formatação eficiente no Java

Uma das maiores fontes de erros e ineficiência que costumo encontrar em sistemas Web escritos em Java é o mau uso das classes de formatação da package java.text.

A documentação avisa que as classes não podem ser usadas concorrentemente. Mas quem lê a documentação? Outro problema é a instanciação excessiva. Tenho a impressão de que ela não ocorre por precaução, mas por indiferença.

Em sistemas Web, principalmente os de alto tráfego, alguns detalhes podem fazer muita diferença. Se instanciar um objeto não custa muito, removê-lo é muito demorado. Num teste simples, descobri que reutilizar uma instância é quatro vezes mais rápido que criar uma nova. E uma instância reutilizada não precisa ser recolhida pelo coletor de lixo.

A solução está na classe ThreadLocal do pacote java.lang. Nem é preciso importar a package! Para a formatação, costumo criar uma classe utilitária da seguinte forma:

import java.text.SimpleDateFormat;
import java.util.Date;

public class Formato {

private static final ThreadLocal formatoData =
new ThreadLocal() {
@Override
protected SimpleDateFormat initialValue() {
SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy");
return df;
}
};

public static SimpleDateFormat getFormatoData() {
return formatoData.get();
}

public static Date data(String value) {
return getFormatoData().parse(value);
}

public static String data(Date value) {
return getFormatoData().format(value);
}

}

Uso nomes bem concisos para os métodos para tornar o código mais simples. O significado fica bastante claro pelo uso:

String hoje=Formato.data(new Date());

Assim, fica garantido que para cada linha de execução haverá apenas uma instância para cada formatação. E todas as operações são feitas usando essa mesma instância. Os servidores de aplicação costumam reaproveitar as threads e então, uma vez criado, cada objeto terá uma longa vida. Além disso, não tem sentido ter muitas linhas de execução por processador. Tipicamente, usam-se 4 ou 8. Se houver mais que isso, o processador vai ficar mais tempo trocando de contexto que fazendo algo útil.

Para executar outras transformações, basta adicionar subclasses de ThreadLocal e os respectivos métodos, conforme o exemplo. Será necessária uma instância para cada tipo de conversão.

quinta-feira, 13 de maio de 2010

Visitor sem accept()

O padrão Visitor é deveras útil, mas pode ser melhorado com um pouco de reflexão. O maior problema dele é que é preciso adicionar um método para cada classe a ser visitada. Geralmente o código não faz mais do que chamar o respectivo método visit() no Visitor. Com reflexão, pode-se eliminar esses métodos. Eliminar código é sempre bom!

Vamos começar pelas classes que serão visitadas.

class Circle { 
  public String toString() { 
    return "Circle"; 
  } 
}

class Square { 
  public String toString() { 
    return "Square"; 
  } 
}

class FunnySquare extends Square { 
  public String toString() { 
    return "FunnySquare"; 
  } 
}

class ReallyFunnySquare extends FunnySquare { 
  public String toString() { 
    return "ReallyFunnySquare"; 
  } 
}

Duas são subclasses de Square e para elas não vou criar métodos visit(). O Visitor com reflexão está no código abaixo.

class Visitor {
  
  private void visit(Circle c) { 
    System.out.printf( "I visited a Circle (%s)\n", c); 
  }
  
  private void visit(Square s) { 
    System.out.printf("I visited a Square (%s)\n", s);
  }

  public void visit(Object e) throws Exception {
    visit(e, e.getClass());
  }
   
  private void visit(Object e, Class c) throws Exception {
    try {
      Method m = getClass().getDeclaredMethod("visit", c);
      m.invoke(this, e);
    } catch(NoSuchMethodException nsme) {
      visit(e, c.getSuperclass());
    } catch(Exception ex) {
      throw ex;
    }
  }
}

O único método público é o visit(Object e). Ele usa uma versão privada que procura um método adequado para a classe do alvo e, se não achar, recursivamente procura métodos usando a superclasse do alvo.

O código abaixo executa um pequeno teste.

Circle c = new Circle();
Square s = new Square();
FunnySquare f = new FunnySquare();
ReallyFunnySquare r = new ReallyFunnySquare();

Visitor v=new Visitor();
for(Object o : new Object[] {
        new Circle(), new Square(), new FunnySquare(), new ReallyFunnySquare()
  }) {
    v.visit(o);
  }

E o resultado é:

I visited a Circle (Circle)
I visited a Square (Square)
I visited a Square (FunnySquare)
I visited a Square (ReallyFunnySquare)

Eu deixei apenas um método visit() público para evitar que o cliente use um método mais abrangente. Como o compilador escolhe o método conforme o tipo da referência e não o tipo do objeto, se for utilizada uma referência com tipo de uma superclasse da instância, perde-se a oportunidade de usar um método mais específico.

Um bônus nisso tudo é que não é preciso usar ifs.

quarta-feira, 12 de maio de 2010

Império dos 8 bits

Diziam os ingleses que o sol nunca se punha no Império Britânico. Eu resolvi descobrir qual a extensão do Império dos 8 bits. Isto é, quais países produziram micros de 8 bits durante a década de 1980.

Comecei pelas Américas e descobri os seguintes países:
  1. Argentina;
  2. Brasil;
  3. Canadá e
  4. Estados Unidos.
Os Estados Unidos, como era de se esperar, desenhou e fabricou a maior parte dos micros. Canadá parece ter tido apenas uma empresa (Semi-Tech) que desenhou um micro (Pied Piper) baseado no Z80 e o produziu nos Estêites. A Argentina tinha 2 fabricantes. A Czerweny fabricava clones do ZX Spectrum (devidamente licenciados) e a Talent/Telematica fabricou 3 modelos de MSX. O Brasil teve um pouco mais de variedade, provavelmente por causa da reserva de mercado.

E o império começa a tomar forma.

domingo, 9 de maio de 2010

Dia da mãe tecnológica

Não tenho paciência com quem tem medo de computador. Geralmente são as mesmas pessoas que ignoram o limite de velocidade na estrada ou tomam o remédio que a vizinha indicou.

Minha mãe começou a usar computador para editar textos no WordWise do BBC B+ com 128KB de RAM. Com ele, edita-se o texto em um modo texto de 40 colunas por 25 linhas. Para saber mais ou menos como vai sair a impressão, usa-se um modo de 80 colunas de 2 cores. Não chegava a ser uma limitação, porque a impressora era matricial (Epson LX80) e monocromática. Havia memória livre para 28KB de texto. Então, para um trabalho grande, era preciso dividir o texto em capítulos. O texto era decorado com palavras-chave que indicavam a formatação. Era quase como programar. Nos disquetes de 5,25" cabiam 400KB.

Depois veio o WordStar. Ainda não era WYSIWYG, mas pelo menos a edição era feita em 80 colunas. Os comandos eram digitados com seqüências estranhas e a mãe os dominava como ninguém: Ctrl-K-B, Ctrl-K-K, etc. A impressora ainda era matricial e os disquetes só continham 360KB.

Finalmente, chegou o Word, começando no Windows 3.11. Desde então, as coisas não mudaram muito, inclusive a resistência de muitos.

Sempre que escuto "estou muito velho para isso" ou "isso é muito difícil", tenho que conter o ímpeto de retrucar com "se até a minha mãe consegue", porque a verdade é que ela tem uma habilidade extraordinária de aprender e não tem medo de enfrentar tecnologia nova.

Feliz dia das mães!

sábado, 8 de maio de 2010

30h de discussão

O controverso (e pouco amado) Sindppd-RS quer reduzir a jornada de trabalho do setor de TI para 30h. É uma discussão que levanta opiniões bem fortes e, em grande parte, pouco elaboradas.

Não me parece absurdo, porque, tendo já trabalhado 30h, sei que é possível fazer bastante com 6h diárias ininterruptas. Mas parece que há um sentimento de que há mais valor em trabalhar muito, como se mais horas criassem exponencialmente mais valor. Pessoalmente, acho que a semana de 40h está de bom tamanho, mas as 40h do hemisfério norte (das 9h às 5h). Lá as pessoas precisam levar os filhos ao colégio, pagar as contas e fazer compras no fim do dia. Aqui, parece que não.

De qualquer forma, não acho que exista um número mágico que seja bom para todos. Por isso, creio que seria mais sensato se as empresas oferecessem vagas com cargas horárias alternativas. Um estudante poderia trabalhar 20h e um pai poderia oferecer 30h. Algumas já fazem isso.

As empresas de TI não costumam vender exatamente o mesmo produto para todos os seus clientes. Não vejo por que tenham que oferecer exatamente o mesmo contrato de trabalho a todos os seus funcionários.

quinta-feira, 6 de maio de 2010

Mais furos no SQL

A primeira solução para encontrar espaços na numeração de uma chave primária tinha um defeito: se não existisse registro para o primeiro número, todos os números até o registro mais baixo permaneceriam vagos.

Pode-se resolver a questão mudando um pouco a consulta. No lugar do olhar para frente, com LEAD(), olha-se para trás, com LAG().


select numero2+livre
from
(select numero numero1,
lag(numero, 1, 0) over (order by numero) numero2
from produtos) p1
inner join (select rownum livre from produtos) p2
on livre < numero1 - numero2
order by 1


Usei lag(numero, 1, 0), porque quero que a numeração comece com 1 (o segundo select usa rownum e este começa com 1).

quarta-feira, 5 de maio de 2010

Furos no SQL

Um dos problemas com os quais mais quebrei a cabeça foi o de encontrar números vagos numa chave primária. A tabela usava como chave apenas um número e para que ele não crescesse indefinidamente (e porque era comum entrarem e saírem registros) eu precisava achar posições vagas para os registros novos.

A solução funcionava, mas era um emaranhado de consultas aninhadas e espertezas numéricas.

Pois bem, com as funções analíticas as coisas ficaram muito mais fáceis!

Suponha, para a consulta abaixo, que eu tenha uma tabela de números de produtos e que quero reaproveitar um número quando um produto desaparecer. Usando a função LEAD(), posso saber, para cada número, qual o próximo número na tabela. Fazendo um join com a própria tabela e fazendo um joguinho rápido com rownum, posso enumerar todos os valores livres, mesmo que os números sejam maiores que o número de registros na tabela. É o caso de haver 100 registros com números do tipo 8 posições; a consulta só vai falhar se o maior furo for maior que o número de registros. Nesse caso, basta achar uma tabela maior para alimentar o rownum (e supondo que seja mesmo necessário achar todos os valores vagos de uma só vez).


select numero1+livre
from
(select numero numero1,
lead(numero, 1, null) over (order by numero) numero2
from produtos) p1
inner join (select rownum livre from produtos) p2
on livre < numero2 - numero1
order by 1


A coluna numero2 tem o próximo valor ocupado e a diferença entre numero2 e numero1 é o número de lugares vagos mais um. No select externo, numero1+livre produz os valores disponíveis.

domingo, 2 de maio de 2010

Pérolas dos usuários III

Arthur C. Clarke escreveu em um de seus livros que "não se pode distinguir de magia qualquer tecnologia suficientemente avançada". Esse momento eu testemunhei muito antes de 3001.

Um belo dia de sol, uma britadeira começou a trabalhar na rua abaixo do escritório. O sistema olhou nos olhos perplexos do usuário. Por que o computador está fazendo esse barulho? Queimará?

A máquina desligou e o barulho continuou. Como pode o sistema fazer barulho com o computador desligado? Mesmo tirando da tomada! Não pode ser! Os elétrons não estão a fluir!

Mas fez.