Elmord's Magic Valley

Software, lingüística e rock'n'roll. Às vezes em Português, sometimes in English.

Posts com a tag: em-portugues

Trivia etymologica #2: cheio, full

2017-03-13 00:17 -0300. Tags: lang, etymology, em-portugues

Seguindo o tema de fartura do último episódio, começaremos nosso passeio etimológico de hoje com a palavra cheio. Cheio vem do latim plenus. É uma mudança fonética e tanto, então vamos por partes.

O meio

Cheio em espanhol é lleno. Olhar para o espanhol freqüentemente ajuda a entender a etimologia de uma palavra em português, porque o espanhol conserva alguns sons que se perderam em português, e vice-versa. Neste caso, o espanhol conservou o n de plenus que o português perdeu. O português tem uma séria mania de perder os sons l e n entre vogais do latim. Exemplos:

Quando as vogais antes e depois do l ou n deletado são iguais, o português junta as duas numa só:

E entre certos encontros de vogais (aparentemente entre ea e entre eo), o português prefere inserir um i (produzindo eia, eio):

Por outro lado, quando o l ou n era geminado em latim, isto é, ll e nn, ele é mantido como l e n em português. Já em espanhol, o som resultante do ll do latim continua sendo escrito como ll, mas é pronunciado como o lh do português (com variações dependendo do dialeto), e o nn do latim se torna o ñ do espanhol (pronunciado aproximadamente como o nh do português). Exemplos:

O início

A outra diferença entre cheio e lleno é o som inicial. Nesse caso, nem o português nem o espanhol conservaram o som original do latim, mas pl e cl quase sempre viram ch em português e ll em espanhol. Outros exemplos:

Provavelmente havia alguma peculiaridade na pronúncia desses encontros em latim (ou pelo menos em alguns dialetos de latim), porque eles sofrem modificações em diversas outras línguas românicas também; em particular, em italiano as palavras acima são chiave (o ch tem som de k), chiamare, pioggia e piorare. No geral, o que se observa é uma palatalização desses encontros consonantais: a pronúncia deles tendeu a evoluir para um ponto de articulação mais próximo do palato (céu da boca). Meu palpite é que o l nesses encontros tinha um som mais próximo do nosso lh nesses dialetos (i.e., /klʲavis, plʲuvia/ (algo como clhávis, plhúvia)).

Surpreendentemente, o francês, que costuma ser uma festa de mudanças fonéticas, mantém ambos os encontros intactos (clef, clamer, pluie, pleurer).

Mudanças regulares

Ok. Como vimos, plenus do latim se torna cheio em português. O que vimos são duas mudanças fonéticas regulares na história do português: uma mudança de pl inicial para ch, e uma perda de n entre vogais. No geral, as mudanças de pronúncia que ocorrem ao longo da história de uma língua têm essa natureza regular: elas afetam certos sons em todas as palavras da língua, sempre que eles ocorrem em determinadas circunstâncias. Claro que como com toda regra, ocasionalmente há exceções, por diversos motivos, mas no geral se espera essa regularidade quando se está formulando uma regra que explique a relação entre duas línguas aparentadas (no caso, latim e português).

Mas espera um pouco, você me diz: o português também possui a palavra pleno (e plenitude), que apresenta tanto o pl inicial quanto o n intervocálico original de plenus. Como é que pode isso?

A resposta nesse caso é que pleno é uma palavra reimportada do latim, depois que esses sound shifts já tinham acontecido. O português (e demais línguas românicas, bem como o inglês) contêm inúmeras palavras desse tipo, tipicamente reintroduzidas a partir da época do Renascimento, quando a cultura clássica grega e romana entrou em alta novamente. O resultado é que freqüentemente o português tem duas versões de uma mesma palavra latina: uma herdada naturalmente do latim, passando pelas mudanças de pronúncia históricas da língua, e uma reimportada diretamente do latim, sem a passagem por essas mudanças. Tipicamente, essas palavras reintroduzidas têm um tom mais formal, enquanto as versões "nativas" têm um tom mais coloquial. Exemplos são cheio e pleno, (lugar) vago e vácuo, chave e clave, paço e palácio. Outras vezes o português tem uma palavra nativa, herdada do latim e passando pelas mudanças fonéticas esperados, e diversas palavras relacionadas importadas posteriormente do latim sem essas mudanças. Exemplos são vida (do latim vita) e vital; chuva (do latim pluvia) e pluvial; mês (do latim mensis) e mensal.

Ok, plenus

Vamos voltar para o latim. Plenus em latim significa – adivinhem só – cheio. Em latim também há um verbo relacionado, pleo, plere, que significa encher. O particípio desse verbo é pletus, que, combinado com a típica variedade de prefixos, nos dá palavras como repleto, completo (e, em inglês, deplete). De in- + plere obtemos implere, que é a origem de encher. De sub- + pleo obtemos supplere, de onde vem suprir (e supply, e suplemento). De com/con- + pleo obtemos complere, de onde vem cumprir (além de completo, e complemento).

Em última instância, pleo vem da raiz indo-européia *pleh₁. Outras palavras dessa mesma raiz são plus (e duplus, triplus, etc.) e polys em grego (de onde vem o nosso prefixo poli-).

Há muitas mil eras eu falei por aqui sobre a Lei de Grimm, que descreve a evolução de alguns sons do proto-indo-europeu nas línguas germânicas. Como visto lá, o p do proto-indo-europeu corresponde ao f nas línguas germânicas. E sure enough, full e fill vêm da mesma raiz *pelh₁ que nos dá pleno e cheio. Viel do alemão também vem da mesma raiz. (Esse v se pronuncia /f/. Curiosamente, o alemão vozeou o /f/ para /v/ antes de vogais na Idade Média e shiftou ele de volta para /f/ alguns séculos depois.)

E a essa altura, já estamos todos cheios do assunto.

1 comentário / comment

Trivia etymologica #1: much vs. mucho

2017-03-05 01:43 -0300. Tags: lang, etymology, em-portugues

Há muitos mil anos, explicando para alguém a diferença entre muy e mucho em espanhol, eu fiz uma analogia com o inglês: muy é como very em inglês, e modifica adjetivos e advérbios, e.g., muy rápido = very fast. Já mucho é como many em inglês, e modifica substantivos: muchas cosas = many things. Na verdade essa explicação é parcialmente correta: muchos no plural é como many, e é usado para substantivos contáveis (como em muchas cosas). Mas mucho no singular é como much, e é usado para substantivos não-contáveis, e.g., mucho dinero = much money.

Na época eu me perguntei: será que much e mucho são cognatas? Apesar da similaridade de som e significado, seria pouco provável que as duas palavras fossem cognatas (a menos que o inglês tivesse importado a palavra do espanhol, o que também seria pouco provável). Isso porque se houvesse uma raiz indo-européia comum para as duas palavras, ela teria passado por mudanças fonéticas diferentes no caminho até o inglês e até o espanhol, e seria pouco provável que o resultado final se parecesse tanto em ambos os branches.

As it turns out, as palavras realmente não são cognatas. much vem do inglês médio muche, muchel, do inglês antigo myċel, miċel. O ċ (som de "tch", ou /tʃ/ em IPA) do Old English é resultado de um shift de /k/ para /tʃ/ diante de /e/ e /i/. Assim, a palavra original era algo como /mikel/, que, em última instância, vem da raiz indo-européia *méǵh₂-, que significa 'grande'. É a mesma raiz de mega em grego, e de magnus em latim. *méǵh₂- com o sufixo *-is, *-yos resulta em latim nas palavras magis, que é a origem da palavra mais em português, e maior, que é a origem de (quem imaginaria?) maior em português. Resumindo, much, mega, mais e maior são todas cognatas.

Mucho, por outro lado, assim como o muito do português, vem do latim multus, que é também a origem do prefixo multi e de palavras como múltiplo. Segundo nosso amigo Wiktionary, multus vem da raiz indo-européia *mel-, e é cognata de melior (de onde vem melhor), que nada mais é do que *mel- com o mesmo sufixo *-yos que transforma mag(nus) em magis e maior.

Por fim, muy é uma contração do espanhol antigo muito, cuja derivação é trivial e sugerida como exercício para o leitor.

1 comentário / comment

elmord.org

2017-02-10 23:52 -0200. Tags: about, comp, web, em-portugues

Então, galere: este blog está em vias de se mudar para elmord.org/blog. Eu ainda estou brincando com as configurações do servidor, mas ele já está no ar, e eu resolvi avisar agora porque com gente usando já tem quem me avise se houver algo errado com o servidor novo.

(Incidentalmente, eu também me mudei fisicamente nas últimas semanas, mas isso é assunto para outro post.)

Mas por quê?

Eu resolvi fazer essa mudança por uma porção de motivos.

O plano qüinqüenal

Eu ainda não sei se essa migração vai ser definitiva (vai depender da estabilidade e performance do servidor novo), então pretendo fazê-la em dois passos:

Servidor novo como réplica do atual. Inicialmente, tanto o elmord.org/blog quanto o inf.ufrgs.br/~vbuaraujo/blog vão ficar servindo o mesmo conteúdo. As páginas do elmord.org ficarão apontando o inf.ufrgs.br como link canônico, i.e., search engines e afins vão ser instruídos a continuar usando a versão no inf.ufrgs.br como a versão "oficial" das páginas. Os comentários de ambas as versões serão sincronizados periodicamente (o que dá para fazer com rsync porque os comentários são arquivos texto).

Redirecionamento para o servidor novo. Se daqui a alguns meses eu estiver suficientemente satisfeito com o funcionamento do servidor novo, ele passa a ser o oficial, as páginas param de incluir o header de link canônico para o blog antigo, e o blog antigo passa a redirecionar para as páginas correspondentes do novo. Se eu não me satisfizer com o servidor novo, eu tiro ele do ar, o inf.ufrgs.br continua funcionando como sempre, e fazemos de conta que nada aconteceu.

EOF

Por enquanto é só. Se vocês encontrarem problemas com o site novo, queiram por favor reportar.

6 comentários / comments

Truques com SSH

2017-01-24 19:40 -0200. Tags: comp, unix, network, em-portugues

Pous, nos últimos tempos eu aprendi algumas coisinhas novas sobre o SSH. Neste post relato algumas delas.

Port forwarding

O SSH possui duas opções, -L e -R, que permitem encaminhar conexões de uma porta local para um host remoto e vice-versa.

Porta local para um host remoto

Imagine que você está na sua máquina local, chamada midgard, e há uma máquina remota, chamada asgard, que é acessível por SSH. Você quer acessar um serviço na pora 8000 da máquina asgard a partir da máquina midgard, mas você quer tunelar o acesso por SSH (seja porque você quer que o acesso seja criptografado, ou porque a porta 8000 simplesmente não é acessivel remotamente). Você pode usar o comando:

midgard$ ssh -L 9000:127.0.0.1:8000 fulano@asgard

O resultado disso é que conexões TCP feitas para sua porta local 9000 serão tuneladas através da conexão com fulano@asgard para o endereço 127.0.0.1, porta 8000 na outra ponta. Por exemplo, se asgard tem um servidor web ouvindo na porta 8000, agora você vai poder abrir um browser em midgard, apontar para http://localhost:9000, e a conexão vai cair na porta 8000 de asgard, tudo tunelado por uma conexão SSH.

Note que o 127.0.0.1 é o endereço de destino do ponto de vista do servidor. Você poderia usar outro endereço para acessar outras máquinas na rede do servidor. Por exemplo, se a máquina vanaheim é acessível a partir de asgard, você poderia rodar:

midgard$ ssh -L 9000:vanaheim:8000 fulano@asgard

e agora todos os acessos à porta TCP 9000 da sua máquina local cairão na porta 8000 de vanaheim, tunelados através da conexão SSH com asgard.

Opcionalmente, você pode especificar um "bind address" antes da porta local, para especificar que apenas a porta 9000 de uma interface de rede específica deve ficar ouvindo por conexões. Por exemplo, você pode usar:

midgard$ ssh -L localhost:9000:vanaheim:8000 fulano@asgard

para dizer que a porta deve escutar apenas conexões da própria máquina. (Por padrão, que interfaces serão usadas é decidido pela opção GatewayPorts do cliente SSH, que defaulta para ouvir apenas na interface local de qualquer forma.) Alternativamente, pode-se passar um bind address vazio (i.e., :9000:vanaheim:8000, sem nada antes do primeiro :), para ouvir em todas as interfaces. Dessa maneira, outras máquinas na sua rede local que acessem a porta 9000 de midgard também terão o acesso tunelado para a porta 8000 de asgard. (* também funciona ao invés da string vazia, mas aí você tem que escapar o * para o shell não tentar expandir.)

Porta remota para a máquina local

Também é possível fazer o contrário: instruir o servidor SSH remoto a redirecionar alguma de suas portas para uma máquina e porta acessível a partir da sua máquina local. Para isso, utiliza-se a opção -R. Por exemplo:

midgard$ ssh -R 8000:localhost:22 fulano@asgard

Isso faz com que a porta 8000 em asgard seja tunelada para a porta 22 da máquina local. Agora, se alguém na máquina asgard acessar a porta 8000 (por exemplo, com ssh -p 8000 beltrano@localhost), a conexão vai cair na sua porta 22 local (e a pessoa terá acesso ao seu servidor SSH local). Você pode usar isso se você está atrás de um firewall ou NAT e a máquina remota é acessível pela Internet, mas a sua máquina local não, e você quer dar acesso a algum serviço da sua máquina local à máquina remota. (Já abordamos isso por aqui antes, mas menciono de novo for completeness.)

Proxy SOCKS

O SSH é capaz de funcionar como um proxy SOCKS. Para isso, utiliza-se a opção -D ("dynamic forwarding"):

midgard$ ssh -D localhost:8000 fulano@asgard

Isso faz com que o SSH ouça como um servidor SOCKS na porta 8000 da máquina local. Conexões recebidas nessa porta serão tuneladas para a máquina asgard, que funcionará como um proxy. Você pode então apontar o proxy SOCKS do seu browser ou outra aplicação para localhost, porta 8000.

Outras opções úteis

-C habilita compressão da conexão. E útil principalmente com conexões lentas (numa rede local, a compressão não compensa muito).

Por padrão, se você usa um dos comandos de redirecionamento de portas acima, o SSH faz o redirecionamento e abre uma sessão de shell comum. Se você quer apenas fazer o redirecionamento, pode usar as opções -N (não executa comando remoto) e -f (vai para background (forks) depois de pedir a senha). As opções podem ser combinadas em um único argumento (e.g., -CNf).

Escapes e comandos especiais

Em uma sessão SSH, a seqüência ENTER ~ é reconhecida como um prefixo de escape para acessar uma série de comandos especiais. Se você digitar ENTER ~ ?, verá uma lista de todos os comandos disponíveis:

Supported escape sequences:
 ~.   - terminate connection (and any multiplexed sessions)
 ~B   - send a BREAK to the remote system
 ~C   - open a command line
 ~R   - request rekey
 ~V/v - decrease/increase verbosity (LogLevel)
 ~^Z  - suspend ssh
 ~#   - list forwarded connections
 ~&   - background ssh (when waiting for connections to terminate)
 ~?   - this message
 ~~   - send the escape character by typing it twice
(Note that escapes are only recognized immediately after newline.)

O comando ENTER ~ C abre um prompt onde é possível fazer e cancelar redirecionamentos de porta, com uma sintaxe análoga à das opções vistas anteriormente:

ENTER ~ C
ssh> ?
Commands:
      -L[bind_address:]port:host:hostport    Request local forward
      -R[bind_address:]port:host:hostport    Request remote forward
      -D[bind_address:]port                  Request dynamic forward
      -KL[bind_address:]port                 Cancel local forward
      -KR[bind_address:]port                 Cancel remote forward
      -KD[bind_address:]port                 Cancel dynamic forward

Pasmem.

Observações

O uso das opções de redirecionamento pode ser controlado/desabilitado na configuração do servidor. Consulte a man page sshd_config(5) para mais informações.

2 comentários / comments

Guile: primeiras impressões

2017-01-02 22:54 -0200. Tags: comp, prog, scheme, lisp, em-portugues

Até agora, as únicas implementações de Scheme com as quais eu tinha tido um contato mais extensivo foram o Chicken e, em menor grau, o Racket. Semana passada eu comecei a dar uma olhada no manual do Guile, o Scheme do Projeto GNU. So far, o Guile pareceu um Scheme bem bacaninha. Neste post, deixo registradas algumas das minhas impressões iniciais do Guile e coisas que eu achei interessantes até agora, com o caveat de que eu ainda não usei o Guile para nada na prática além de testar meia dúzia de coisas no REPL e escrever um ou outro script de meia dúzia de linhas.

Bytecode

Diferente do Chicken, o Guile não gera executáveis nativos; ao invés disso, ele compila para um bytecode próprio. Na verdade, a VM do Guile suporta não apenas Scheme, como também possui suporte preliminar a Emacs Lisp e ECMAScript (!), mas ainda não sei como essas coisas se integram. Em termos de performance, o Guile não parece ser nem lá nem cá, e imagino que seja comparável a outras linguagens interpretadas, como Python. Eu experimentei fazer uns benchmarks toscos, mas os resultados foram inconclusivos e requererão uma análise mais aprofundada, que eu não hei de fazer tão cedo.

Debugabilidade

Em termos de debugabilidade, o Guile ganha bonito do Chicken. Para começar, o Guile imprime (pasmem!) um stack trace quando ocorre um erro. O Chicken não imprime um stack trace pelo simples fato de que ele não usa uma pilha de chamadas da maneira convencional; quando ocorre um erro, o Chicken imprime um "histórico de chamadas", i.e., uma lista das últimas N chamadas, na ordem em que ocorreram, mas sem representar o aninhamento das chamadas, o que torna a depuração mais complicada. Além de mostrar uma pilha, o Guile ainda mostra os valores dos argumentos em cada chamada empilhada (algo cuja falta me incomoda bastante em Python) e, quando executado em modo interativo, cai num debugger que permite, entre outras coisas, inspecionar os valores das variáveis locais. Também é possível definir breakpoints e essas coisas que se espera de um debugger, mas não cheguei a olhar essa parte com calma.

Além disso, o Guile tende a detectar mais erros do que o Chicken. Por exemplo, o Chicken não reporta um erro se uma função é declarada com múltiplos parâmetros com o mesmo nome, ou se uma função é chamada com um keyword argument que ela não suporta.

(Não-)minimalismo

No Chicken há uma separação maior entre uma linguagem core pequena e extensões, que têm que ser importadas explicitamente em programas que as usam. (Por exemplo, no programa de adivinhações de um post anterior, foi necessário dar um (use extras) para ter acesso à função random.) No Guile, uma quantidade bem maior de funcionalidades (incluindo expressões regulares e a API POSIX) já está disponível mesmo sem fazer nenhum import. Nesse quesito, o Guile tem um feel um pouco mais "Common-Líspico" do que o Chicken. (Mas não muito; coisas como orientação a objetos requerem um import explícito.)

Um outro sentido em que o Guile é não-minimalista é que freqüentemente há multiplas APIs para fazer a mesma coisa. Em muitos casos, isso se deve ao fato de que uma API nova foi introduzida (freqüentemente uma SRFI, o que é um ponto positivo), mas a antiga foi mantida por compatibilidade. Por exemplo, para a definição de estruturas, o Guile suporta a SRFI-9, as APIs tradicionais do Guile (anteriores à SRFI-9) e a API de records do R6RS. Da mesma forma, o Guile suporta escopo dinâmico tanto por meio de fluids (a interface histórica) quanto por parameters (SRFI-39). (Os parameters são implementados em termos de fluids no Guile.)

O Guile parece ser bastante comprometido com compatibilidade com versões anteriores, o que tem o ponto bem positivo de que seu código provavelmente vai continuar funcionando nas versões futuras, mas isso vem com o custo de ter múltiplas APIs para as mesmas funcionalidades hanging around.

Módulos

Enquanto o Chicken faz uma distinção entre units (que são usadas para compilação separada de partes de um programa) e módulos (que são usados para isolar namespaces), no Guile um módulo serve a ambos os propósitos. Na verdade eu acho essa distinção que o Chicken faz bastante annoying (e aparentemente há quem queira deprecar as units no Chicken 5), e mui me alegrou saber que o Guile (1) possui um sistema de módulos; (2) que não é cheio de frescura (ou pelo menos as frescuras são opcionais); e (3) é fácil de usar.

O nome de um módulo em Guile é uma lista de símbolos, e um módulo de nome (foo bar) é procurado no arquivo load_path/foo/bar.scm. O load path default pode ser alterado através de um parâmetro da linha de comando, ou de uma variável de ambiente, ou setando %load-path e %load-compiled-path explicitamnte.

Não sei qual é a maneira convencional de escrever programas separados em múltiplos arquivos sem ter que instalá-los no load path. Imagino que uma maneira seja escrever um arquivo main que sete o load path para incluir o diretório do programa, e depois importar os demais componentes do programa. Outra maneira é dar include nos arquivos, mas isso não cria módulos com namespaces separados.

Threads

O Guile suporta threads nativas do sistema operacional, diferentemente do Chicken, que suporta apenas "green threads" (uma thread nativa rodando múltiplas threads lógicas cooperativamente). Além das APIs comuns para criação de threads, mutexes e toda essa bagulheira, o Guile também suporta uma API de futures, mantendo automaticamente uma pool de threads cujo tamanho é determinado por padrão pelo número de cores da máquina, e uma macro (parallel exp1 exp2 ...) que roda todas as expressões em paralelo e retorna o valor de cada uma, e um letpar, um "let paralelo" que avalia o valor de todas as variáveis em paralelo. Não sei quão útil isso é na prática, mas que é bem legal, é.

Orientação a objetos

O Guile vem com um sistema de orientação a objetos baseado em generic functions e multiple dispatch a la CLOS, chamado GOOPS. Ainda não olhei o GOOPS com calma, mas ele parece não ter todas as coisas que o CLOS tem (por exemplo, before, after e around methods), mas ele permite redefinir classes em tempo de execução (com atualização automática das instâncias existentes da classe), e parece ter algumas coisinhas a mais (e.g., provisões para mergear métodos de mesmo nome herdados de módulos diferentes).

Uma coisa muito legal do GOOPS em comparação com o CLOS é que ele permite transformar transparentemente uma função comum em uma função genérica. Por exemplo, você pode adicionar um método à função builtin +:

(define-method (+ (a <string>) (b <string>))
  (string-append a b))

Feito isso, agora você pode escrever (+ "a" "b"), e o resultado será "ab". O interessante disso é o define-method não sobrepõe o + existente com um + novo: ele modifica o + existente, e agora todo o código que usava + antes vai passar a usar esse + aumentado. Aparentemente isso só funciona para substituir funcionalidades não-padrão dos operadores; se você definir um método (+ (a <number>) (b <number>)) e tentar somar dois números, o Guile vai continuar usando a soma padrão ao invés da sua definição, acredito eu que porque a chamada a + é compilada para a instrução add da VM, que vai ignorar o método caso os argumentos sejam números. (O que de certa forma torna o fato de o + usar o método definido pelo usuário quando os argumentos não são números ainda mais impressive, pois, eu suponho, eles tiveram que "go out of their way" para fazer a instrução add da VM verificar se houve a adição de métodos ao + pelo usuário quando os argumentos não são números. Mas não sei o suficiente sobre a implementação do Guile para saber realmente o que está acontecendo por baixo dos panos.)

Setters

Uma coisa que eu achei meio chata no Guile com relação ao Chicken é que o Guile não suporta "de fábrica" usar set! em coisas que não sejam identificadores. Por exemplo, no Chicken é possível escrever coisas como (set! (car x) 42) ao invés de (set-car! x 42); o Guile, por padrão, não tem suporte a isso. O Guile tem suporte a "procedures with setters", através de uma API tradicional e da API da SRFI-17, e ao importar o módulo (srfi srfi-17) o set! passa a ser usável com car, cdr e vector-ref, mas tem um zilhão de outras funções similares (como hash-ref, array-ref, etc.) que não têm setters definidos. Nada fatal, e nada lhe impede de definir os setters para essas funções, mas seria legal se houvesse suporte nativo a setters para todas as funções em que faz sentido ter um setter, como é o caso no Chicken.

Bibliotecas

O Guile parece ter bem menos bibliotecas do que o Chicken, e certamente não possui um repositório centralizado de bibliotecas, como é o caso dos eggs do Chicken. (A documentação do guild, a interface para os utilitários de linha de comando do Guile, tais como guild compile, menciona planos de permitir instalar pacotes da Internet através do guild no futuro. Não sei como eles pretendem realizar isso, mas, da minha parte, eu acho que mais importante do que um repositório centralizado é uma maneira padronizada de empacotar programas/bibliotecas e descrever dependências de uma maneira que permita sua resolução automática na instalação. But I digress.)

Por outro lado, o Guile vem com uma porção de módulos de fábrica, e possui bindings para a Gtk e o GNOME. Ainda não as olhei com calma, mas pode ser uma solução interessante para criar aplicações com interface gráfica.

Unicode

No Chicken, por padrão, todas as strings são strings de bytes. Há um módulo/extensão/unit/library/whatever chamada utf8, que reimplementa diversas funções de manipulação de strings para assumirem que as strings estão codificadas em UTF-8 (as strings continuam sendo strings de bytes por baixo dos panos). Importar o utf8 não substitui, mas sim redefine, as funções padrão, então, pelo que eu entendo, importar utf8 no seu módulo não vai fazer os outros módulos do sistema que não importaram explicitamente utf8 passarem a funcionar magicamente com strings UTF-8.

No Guile, strings são Unicode nativamente (há um tipo separado para "byte vectors", que pode ser usado para guardar bytes literais não interpretados como caracteres). Portas (arquivos abertos) possuem um encoding associado, e o Guile faz a conversão de Unicode para o encoding da porta automaticamente. Não sei se isso não pode acabar incomodando na prática (o encoding default é determinado pelo locale, e modo de abertura de arquivos que depende do locale me dá um certo medo, mas talvez seja por trauma dos UnicodeDecodeError do Python 2, o que não é a mesma situação do Guile porque no Guile todas as strings são Unicode por padrão; e nada impede de setar o encoding manualmente ao abrir um arquivo).

Conclusão

No geral, o Guile me pareceu uma implementação bem legal de Scheme, e tem um monte de outros aspectos interessantes que eu não cheguei a mencionar nesse post (por exemplo, que ele foi feito para ser embutível em programas C, e que a API C é documentada juntamente com as funções correspondentes em Scheme, e que no geral a documentação do Guile é bem boa). Quero ver se o uso em projetos no futuro para ter uma experiência mais prática com ele.

2 comentários / comments

Essa vida mulambda

2016-09-14 23:23 -0300. Tags: about, life, mind, em-portugues

Pois então, galera. Faz um bocado de tempo que eu não dou as caras por estas terras de Entre-Douro-e-Minho, por uma porção de motivos.

Emprego

Eu comecei a trabalhar no final de julho, o que me deixou com menos tempo livre para dedicar às atividades blogareiras. Além disso, eu chego em casa com variáveis graus de cansaço, e conseqüentemente menos inclinado a sentar e escrever. (Ultimamente eu tenho chegado menos cansado do que inicialmente, talvez porque eu agora eu já tenha me acostumado melhor com a rotina. Mas eu ainda continuo chegando com uma certa dose de zonzeira na cabeça, o que eu desconfio que tem mais a ver com o barulho do ônibus e do ar-condicionado do serviço do que com cansaço em si, mas ainda não testei essa hipótese.)

Quanto ao trabalho, está sendo bem bacaninha. Estou trabalhando como desenvolvedor Python, que das linguagens mais mainstream acho que é a que eu mais gosto, e tenho aprendido bastante coisa sobre as tecnologias da modinha (plot twist: elas são úteis). A única coisa não muito empolgante é que estamos trabalhando sobre uma codebase herdada de uma outra empresa, e que foi desenvolvida usando técnicas de programação um tanto quanto, digamos, interessantes.

Outra coisa que eu descobri por lá é que o futuro do subjuntivo irregular ("fizer", "tiver", etc.) está mais morto do que eu pensava, mas isso é assunto para outra ocasião.

Mestrado

Outra coisa que eu tinha (e tenho) tomando meu tempo livre é o mestrado, e o tempo que não era comido pelas atividades do mestrado era comido pela preocupação de achar que devia estar fazendo as atividades do mestrado. Depois de ter tido um segfault na cabeça no fim-de-semana retrasado tentando terminar um paper para semana passada, eu decidi parar de me preocupar com o mestrado por enquanto, o que em termos de saúde mental parece ter sido uma ótima decisão. Eu pretendia falar um pouco mais sobre a situação do mestrado, mas acho que vai ficar para depois de eu o ter concluído (ou de eu ter sido chutado dele por timeout).

Mudança de workflow

Normalmente, meu workflow para escrever um post é: (1) pegar um tópico; (2) escrever sobre o tópico até exaustão (tanto do tópico quanto do autor) por qualquer quantidade de tempo entre duas e oito horas; (3) reler o post (com variáveis graus de exaustividade) para ver se ficou algum erro de digitação ou redação; (4) publicar o post (e descobrir erros depois anyway). Às vezes eu até começo em um dia e continuo em um outro, mas no geral vai tudo em uma sentada. Uma conseqüência disso é que eu já sei de antemão que escrever um post vai tomar um bocado de tempo, então eu acabo só me sentindo inclinado a escrever quando eu sei que vou ter rios de tempo livre sem interrupções. (Outra conseqüência disso são os famosos posts de 20k, tão apreciados pelos leitores.)

Agora que eu não tenho mais rios de tempo livre contíguo com tanta freqüência, esse workflow está sendo um tanto quanto sub-ótimo (vide quantidade de posts nos últimos dois meses). Estou pensando se não é o caso de eu começar a escrever posts menores e splitar tópicos em múltiplos posts, mas não sei até que ponto isso é uma boa idéia. Faz umas semanas que eu tenho pensado em escrever um post sobre alguns fatos [que eu acho] interessantes sobre os números nas línguas indo-européias, mas isso é um post que tomaria bastante tempo tanto para a escrita em si quanto para pesquisar/conferir os fatos antes de escrever. Não sei se faz sentido separar o tópico em múltiplos posts e ir postando uma série de posts pequenos, ou se é melhor escrever um post só em várias sentadas e publicar tudo de uma vez no fim do ano. Eu prefiro ler esse tipo de informação toda de uma vez, mas eu sei que pelo menos alguns dos leitores não partilham da mesma preferência. (On the other hand, eu não sei se esses mesmos leitores leriam a mesma informação espalhada em múltiplos posts. So there's that.)

Mas essa é a sua opinião, ele já tem outra opinião, ué. Qual a sua opinião?

On another note

Unrelated com qualquer coisa, por ser ano eleitoral, eu implementei a mui lendária e prometida lista de comentários recentes na sidebar do blog no domingo passado. Espero que ela traga muitas alegrias a todos. Votem em mim para déspota universal.

12 comentários / comments

And so it goes

2016-07-15 14:40 -0300. Tags: music, lang, em-portugues

Fazia muitos mil kalpas que eu queria achar a letra em cantonês da música tema do filme Tai Chi Master (太極張三豐). No fim consegui achar vendo um pedaço do filme com legendas em chinês, transcrevendo parte da legenda e procurando nas interwebs. A letra veio daqui, e a transcrição em Jyutping daqui. (As transcrições em vermelho são as que eu corrigi conferindo no Wiktionary as que eu achava que estavam erradas.) Aqui tem uma versão com legendas em inglês do trecho da música que aparece no filme, mas não sei quão confiável é a legenda.

zi6seon3sau2zung1bat1gin3goeng6jyu4ging3
zi6jau5sam1zung1jat1pin3wo4jyu4peng4

jik6loi4seon6sau6
hung1heoi1gin3fung1sing4
kong4bou6faa3sing1peng4
mou4lou6cyu2zi6jau5tin1meng6

dung6deoi3zing6
ceoi4deoi3sing4

ceoi4jyun4jap6sai3
jan1fung1ceot1sai3
mou4cing4jik6jau5cing4

ceoi4jyun4seon6sing3
bat1caang1bat1sing1
mou4cing4si6jau5cing4

* * *

daan6gaau3hau2zung1bat1syut3ci4jyu4lim1
daan6gin3sau2zung1nim1gwo1can4sai3cing4

wui6joeng4gap3jam1
sam1ngon1gaau3tin1zing6
jau4joek6gaau3fung1peng4
mou4lou6cyu2zi6jau5tin1meng6

dung6deoi3zing6
ceoi4deoi3sing4

ceoi4jyun4jap6sai3
jan1fung1ceot1sai3
mou4cing4jik6jau5cing4

ceoi4jyun4seon6sing3
bat1caang1bat1sing1
mou4cing4si6jau5cing4

3 comentários / comments

Anúncio: Estou procurando trabalho

2016-07-06 22:27 -0300. Tags: life, about, em-portugues

[Update (20/07/2016): Consegui um emprego. Dismiss!]

Pois então, galera. Por um momento eu achei que ia conseguir uma vaga como professor substituto no INF e todos os meus problemas financeiros estariam resolvidos (pelo menos durante o um ou dois anos de validade do concurso). Alás*, embora eu tenha conseguido um 9,8 na prova escrita do concurso, o pessoal que tinha doutorado passou na minha frente na pontuação por títulos e publicações, e eu acabei em 3º lugar. Ainda pode ser que me chamem no decorrer dos próximos dois anos, mas infelizmente eu preciso comer e pagar as contas nesse meio tempo.

Assim, estou atrás de trabalho. Isso inclui tanto empregos (como desenvolvedor, sysadmin e afins, e também como professor, instrutor, etc.) quanto serviços como free-lancer (com desenvolvimento de programas, sites, instalação e suporte de redes e servidores, e até formatando o PC e desinstalando as toolbars dos seus conhecidos).

Se você souber de alguma oportunidade, ou puder me recomendar para alguma empresa ou conhecido, ou tiver alguma dica, ou qualquer outro feedback, queira entrar em contato (ou passar meu contato, conforme aplicável). Se alguém quiser meu telefone para passar para alguém, pode me pedir por e-mail (no momento eu estou um pouco receoso de pôr meus telefones online, mas talvez eu acabe fazendo isso). Dou preferência para oportunidades na grande Porto Alegre, ou serviços que eu possa realizar remotamente, mas dependendo do emprego posso considerar me mudar.

Meanwhile, eu pretendo voltar a trabalhar em projetos pessoais, pois (1) pode ser que minhas experiências com linguagens de programação rendam alguma coisa que eu possa pôr no curriculum; (2) eu espero aprender um bocado de coisas com eles (em particular, eu tenho pensado em reviver o lows como testbed para experiências com sistemas de tipos e outras features, e para experimentar com técnicas de compilação); e (3) uma sábia esquimó uma vez disse: "Não deixe de fazer as coisas que gosta no presente pra poder fazer elas no futuro – pode ser que o futuro nunca chegue", e cada vez isso me parece mais verdade.

_____

* Turns out que alas vem do francês arcaico "a las". "Las", por sua vez, vem do latim "lassus", cansado (cf. lasso, lassidão). Visto a origem latina da palavra, eu me vejo ainda mais inclinado a incorporar "alás" à língua portuguesa.

5 comentários / comments

Land

2016-06-11 22:50 -0300. Tags: music, life, mind, em-portugues

Eu ia escolher um verso para usar como título do post, mas a letra é toda épica demais pra escolher.

Týr – Land (YouTube)

Homeland we're leaving
We are retrieving
Our right to stand alone
We cannot stay here
Leave this bay
Fear not what must be
We must cross the sea

On our own
Standing alone
Always we got by on our own
Under stormy skies
Through rain, wind and raging sea
Head into the Unknown

Leave behind
Bonds that may bind
Circumstance that keep us behind
Rise to meet the day
Hold high torches passed through time
Fear not what you might find

Ver sterk mín sál á køldu náttarvakt
Har eingi altarljós til gudar brenna
Har hvør ein vón av fannkava var takt
Og hjarta ongan hita meir kann kenna
Ver stór mín sál sum rúmdar kalda tøgn
Ið eina er, tá sloknar lívsins søgn.

[Translation]
Be strong, my soul, on thy night vigil cold
Where to the gods no altar candle burns
Where every hope the snowy drifts enfold
And ne'er a trace of heat my heart discerns
Be great, my soul, as those still spaces cold
Which lone remain, when life's brief tale is told

Roads are long and oceans far and wide
Nights are cold and skies are dark and gray
Ride the autumn wind and evening tide
Time is long and land is far away

Out on the sea
Waiting for me
Storms are raging violently
Still we sail on silently
We seek to tame the torrrents and tides
Master the mights

Sail with me across the raging sea
Write your tale into eternity
Still we've sighted only sea till now
As we sail, I sometimes wonder how

* * *

Rest in the twilight
I have gained insight
Since the deeds of younger days
Now I am wiser
Raise my eyes
Gaze across the sea
And recall when we

Sailed away
Sought a new way
How I longed for far, far away
In the sunset glow
I dreamt of another land
A thousand years from that day

[from the Hávamál]
Cattle die
Kinsmen will die
I myself must die too some day
All are mortal men
But fair fame will never fade
For the man who wins it

Ver sterk mín sál á mjørkatungu ferð
Har tættar fylkjast um teg gráar gátur
Tín barnaflokkur
Úttærdur hann er
Og sárur kennist hans sólsvangi grátur
Ver stór mín sál í dagsins royndar stund
Holl veitir nátt hin dreymaleysa blund.

[Translation]
Be strong, my soul, upon thy darkling way
Where grey mysterious forms about thee run
Thine offspring, who their weariness display
In piteous weeping for the absent sun
Be great, my soul, with the day's griefs oppressed
A long night comes, to grant the dreamless rest.

Roads are long and oceans far and wide
Nights are cold and skies are dark and gray
Ride the autumn wind and evening tide
Time is long and land is far away

Out on the sea
Waiting for me
Storms are raging violently
Still we sail on silently
We seek to tame the torrents and tides
Master the mights

Sail with me across the raging sea
Write your tale into eternity
Still we've sighted only sea till now
As we sail, I sometimes wonder how far to Asgaard.

[Letra tomada deste site, com pequenas correções.]

3 comentários / comments

Tour de Scheme, parte 2: Listas, atribuição, e uma agenda de telefones

2016-04-14 20:43 -0300. Tags: comp, prog, lisp, scheme, tour-de-scheme, em-portugues

[Este post é parte de uma série sobre Scheme.]

Saudações, camaradas! Neste episódio, abordaremos um dos principais (se não o principal) tipos de dados de Scheme: a lista. Também vamos ver como fazer atribuição a uma variável, mexer um pouquinho assim com arquivos, e escrever um rico programa de agenda de contatos.

[Screenshot do programa de agenda de contatos]
The Ultimate In Contact Management™

Este post meio que assume que você nunca trabalhou ou já esqueceu como trabalhar com listas em Scheme. Se você já está acostumado com manipulação de listas em linguagens funcionais, muito do que é dito aqui não vai ser novidade. Sugestões de melhorias no texto e comentários no geral são bem-vindos.

Como de costume, eu vou usar o Chicken para rodar os exemplos. Você pode acompanhar os exemplos usando o DrRacket ao invés do Chicken, se preferir, mas para isso você deverá abrir a janela "Choose Language" (Ctrl+L), escolher "The Racket Language", clicar no botão "Show Details", e mudar o Output Style para "write" ao invés de "print".

Pares

Vamos começar por um tipo de dados mais fundamental: o par. Um par é, como o nome sugere, um par ordenado de dois valores quaisquer. O par é o tipo de dados composto mais antigo da família Lisp, e por esse motivo as funções de manipulação de pares possuem uma terminologia meio peculiar vinda diretamente de 1958. Pares são construídos pela função cons:

#;1> (cons 1 2)
(1 . 2)
#;2> (cons 42 'foo)
(42 . foo)

Por esse motivo, pares também são conhecidos como células cons (cons cells).

Uma vez criado, podemos extrair os pedaços de um par com as funções car (que extrai o item do lado esquerdo do par) e cdr (que extrai o item do lado direito):

#;1> (define p (cons 23 42))
#;2> p
(23 . 42)
#;3> (car p)
23
#;4> (cdr p)
42

[Os nomes car e cdr vêm do fato de que o Lisp original rodava numa máquina (o IBM 704) em que os registradores podiam ser separados em uma porção de "endereço" e uma porção de "decremento". As funções de extração dos elementos de um par em Lisp eram implementadas usando essa funcionalidade: car era a sigla de "Content of Address of Register", e cdr de "Content of Decrement of Register". Hoje em dia, esses nomes persistem por tradição (a.k.a. todo o mundo já estava acostumado com eles e ninguém quis mudar). No geral, nós simplesmente pensamos em car e cdr como os nomes dos componentes de um par em Lisp/Scheme (assim como nós normalmente usamos o comando cat no Unix sem pensar no fato de que o nome vem de "concatenate").]

A função pair? testa se um valor é um par:

#;1> (pair? (cons 23 42))
#t
#;2> (pair? 23)
#f

Por sinal, eu não mencionei isso até agora, mas existem funções análogas para cada tipo de Scheme (e.g., number? testa se um valor é um número, symbol? testa se é um símbolo, string? se é uma string, etc.). Funções que retornam verdadeiro ou falso são chamadas de predicados em Scheme. Por convenção, a maioria dos predicados em Scheme têm nomes terminados em ?.

Naturalmente, os elementos de um par podem ser outros pares:

#;1> (define p (cons (cons 23 42) (cons 69 81)))
#;2> (car p)
(23 . 42)
#;3> (car (car p))
23
#;4> (cdr (car p))
42
#;5> (cdr p)
(69 . 81)
#;6> (car (cdr p))
69
#;7> (cdr (cdr p))
81

E nada nos impede de colocar pares dentro de pares dentro de pares, o que nos permite criar tripas intermináveis de pares, também conhecidas como...

Listas

Embora pares sirvam para guardar, bem, pares de qualquer coisa, o uso mais comum de pares em Scheme é para criar listas encadeadas (conhecidas simplesmente como listas em Scheme). A idéia é que no car de um par guardamos um elemento da lista, e no cdr do par guardamos o restante da lista, que é outro par, contendo mais um elemento e o restante da lista, e assim por diante. Por exemplo se queremos criar uma lista com os números 23, 42 e 81, criamos:

Essa coisa que não seja um par poderia ser um valor qualquer que nos indicasse que chegamos ao fim da lista; poderíamos usar o símbolo 'fim, por exemplo, e escrever:

(cons 23 (cons 42 (cons 81 'fim)))

Porém, o Scheme possui um valor criado especificamente para indicar o final de uma lista: a lista vazia, que pode ser escrita como '():

#;1> '()
()

Assim, podemos escrever:

(cons 23 (cons 42 (cons 81 '())))

Quem está começando na linguagem freqüentemente olha para uma expressão como essa e simplesmente lê linearmente "cons 23, cons 42, cons 81, lista vazia" e ignora os parênteses. Procure prestar atenção no aninhamento: a expressão como um todo é (cons 23 algo) (i.e., um par cujo car é 23 e o cdr é algo), onde algo é a sub-expressão (cons 42 mais-algo), e assim por diante.

Como esse uso de pares aninhados para representar listas é muito freqüente em Scheme, a linguagem usa uma notação abreviada ao imprimi-los. Basicamente, se o cdr de um par é outro par, ao invés de imprimir algo como (foo . (bar . baz)), o Scheme omite o ponto que separa o car do cdr e os parênteses do cdr, e escreve (foo bar . baz):

#;1> (cons 23 (cons 42 81))
(23 42 . 81)
#;2> (cons 23 (cons 42 (cons 81 'fim)))
(23 42 81 . fim)

Além disso, se o cdr de um par é a lista vazia, ao invés de imprimir algo como (foo . ()), o Scheme omite o cdr inteiro e escreve (foo):

#;1> (cons 23 '())
(23)
#;2> (cons 23 (cons 42 '()))
(23 42)
#;3> (cons 23 (cons 42 (cons 81 '())))
(23 42 81)
#;4> (cons 'foo (cons 'bar '()))
(foo bar)

Para construir listas com vários elementos, ao invés de escrever manualmente um monte de chamadas a cons aninhadas, podemos usar a função list, que recebe zero ou mais argumentos e devolve uma lista com os elementos especificados:

#;1> (list 23 42 81)
(23 42 81)
#;2> (list 'foo 'bar 'baz)
(foo bar baz)

Por baixo dos panos, todavia, o que essa função faz é simplesmente construir os pares aninhados que estávamos construindo com cons, e as funções car e cdr podem ser usadas para extrair os componentes da lista (lembrando que (foo bar baz) é simplesmente abreviação de (foo . (bar . (baz . ()))):

#;1> (define lista (list 'foo 'bar 'baz))
#;2> lista
(foo bar baz)
#;3> (car lista)
foo
#;4> (cdr lista)
(bar baz)
#;5> (car (cdr lista))
bar
#;6> (cdr (cdr lista))
(baz)
#;7> (car (cdr (cdr lista)))
baz
#;8> (cdr (cdr (cdr lista)))
()

O predicado null? testa se um valor é a lista vazia:

#;1> (define lista (list 23 42))
#;2> lista
(23 42)
#;3> (null? lista)
#f
#;4> (cdr lista)
(42)
#;5> (cdr (cdr lista))
()
#;6> (null? (cdr (cdr lista)))
#t
#;7> (null? 81)
#f

Scheme vs. HtDP

Se você está vindo das linguagens HtDP, deve ter notado algumas diferenças no tratamento de listas em Scheme R5RS:

Em um post futuro sobre S-expressions, reabordaremos a questão da sintaxe de listas. [Leia-se: eu escrevi 6kB de texto sobre o assunto e vi que não ia sobrar espaço para manipulação de listas e a agenda de contatos.] Por ora, vamos seguir o baile.

Uma agenda de contatos

Ok, galera. Tentando seguir a filosofia mão-na-massa dos posts anteriores, vamos começar a implementar nossa agenda de contatos. Nosso programa deverá oferecer um menu ao usuário, permitindo inserir um contato, listar todos os contatos, procurar um contato pelo nome, remover um contato, e sair do programa. Vamos começar escrevendo duas rotinas recorrentes de interação: uma função que imprime um prompt e lê uma string do usuário, e uma função análoga que lê um número:

(define (lê-string prompt)
  (display prompt)
  (read-line))

(define (lê-número prompt)
  (string->number (lê-string prompt)))

Great. Agora, já podemos começar a implementar o nosso menu. Ele deve imprimir as opções para o usuário, pedir o número da opção desejada e, dependendo da escolha, chamar a função apropriada. Nós poderíamos fazer isso assim:

(define (menu)
  (display "=== Menu ===
  (1) Inserir contato
  (2) Listar todos os contatos
  (3) Procurar contato por nome
  (4) Remover contato
  (5) Sair\n")
  (let ([opção (lê-número "Digite uma opção: ")])
    (cond [(= opção 1) (inserir-contato)]
          [(= opção 2) (listar-contatos)]
          [(= opção 3) (procurar-contato)]
          [(= opção 4) (remover-contato)]
          [(= opção 5) (sair)]
          [else (display "Opção inválida!\n")])))

Note que usamos let ao invés de define para armazenar o resultado de lê-número em uma variável local, pois, como já vimos, não podemos/devemos usar define sem ser no começo do corpo da função (ou outras formas especiais que se comportam como o corpo de uma função).

Ao invés de escrevermos um cond que compara um mesmo valor com várias constantes, poderíamos usar a forma case, que é parecida com o switch do C. A sintaxe é:

(case valor
  [(constante1 constante2...) ação-a]
  [(constante3 constante4...) ação-b]
  ...
  [else ação-default])

O nosso menu, então, ficaria assim:

(define (menu)
  (display "=== Menu ===
  (1) Inserir contato
  (2) Listar todos os contatos
  (3) Procurar contato por nome
  (4) Remover contato
  (5) Sair\n")
  (case (lê-número "Digite uma opção: ")
    [(1) (inserir-contato)]
    [(2) (listar-contatos)]
    [(3) (procurar-contato)]
    [(4) (remover-contato)]
    [(5) (sair)]
    [else (display "Opção inválida!\n")]))

Esse seria um bom momento para carregar o código no interpretador e ver se está tudo correto, mas eu vou deixar isso como exercício para o leitor. Note que mesmo não tendo definido ainda as funções inserir-contato, etc., você já pode chamar a função (main) no interpretador (ou até compilar o código, dependendo das circunstâncias); o código vai rodar até o ponto em que a função inexistente for chamada, e então gerar um erro em tempo de execução.

Agora, vamos trabalhar na inserção de contatos. Em primeiro lugar, nós temos que armazenar os contatos em algum lugar; para isso, vamos criar uma variável global contatos, inicializada com a lista vazia, onde nós vamos ir colocando os contatos à medida em que forem adicionados:

(define contatos '())

Em segundo lugar, nós temos que definir como vamos representar um contato. Poderíamos definir uma estrutura de dados para isso, mas como (1) o objetivo hoje é trabalhar com listas, (2) nós ainda não vimos estruturas, e (3) listas podem ser facilmente impressas, o que facilita a nossa vida mais adiante, nós vamos representar um contato como uma lista de dois elementos, em que o primeiro é o nome e o segundo é o telefone, i.e.:

#;1> (define exemplo (list "Hildur" "555-1234"))
#;2> exemplo
("Hildur" "555-1234")

Para acessar o nome, pegamos o primeiro elemento da lista, i.e., o car:

#;3> (car exemplo)
"Hildur"

Para acessar o telefone, pegamos o segundo elemento da lista. Como vimos, o cdr de uma lista é o restante da lista, i.e., a lista sem o primeiro elemento (equivalente ao ponteiro para o próximo elemento de uma lista encadeada em C):

#;4> (cdr exemplo)
("555-1234")

O segundo elemento é o primeiro elemento do restante da lista, então podemos acessá-lo pegando o car do restante (cdr) da lista:

#;5> (car (cdr exemplo))
"555-1234"

Na verdade, esse tipo de acesso usando seqüências de car e cdr é tão comum em Scheme que a linguagem oferece combinações prontas de car e cdr para até quatro níveis de "profundidade":

Na verdade, parte do motivo de os nomes car e cdr terem persistido até os dias de hoje é que nomes alternativos não são tão fáceis de compor. True story.

De qualquer forma, não queremos encher nosso programa de cadrs e cddrs, por dois motivos: primeiro, esses nomes não são exatamente a coisa mais prontamente legível do mundo; e segundo, nós não queremos que o programa fique tão dependente da representação dos contatos: se no futuro nós decidirmos adicionar campos, ou mudar a ordem, ou trocar as listas por estruturas de verdade, não queremos ter que sair mudando todos os pontos do programa que usam contatos. Assim, vamos introduzir um pouquinho de abstração criando funções para criar um contato e obter os campos de um dado contato:

(define (novo-contato nome telefone) (list nome telefone))
(define (nome-do-contato contato) (car contato))
(define (telefone-do-contato contato) (cadr contato))

Agora, podemos escrever (depois de recarregar o código no interpretador):

#;1> (define exemplo (novo-contato "Hildur" "555-1234"))
#;2> exemplo
("Hildur" "555-1234")
#;3> (nome-do-contato exemplo)
"Hildur"
#;4> (telefone-do-contato exemplo)
"555-1234"

(Os nomes ficaram meio verbosos, mas foi o melhor que eu consegui pensar em português no momento.) O importante é que agora toda a manipulação de contatos no resto do código ocorrerá através dessas funções, e se quisermos mudar a representação de um contato só teremos que mudar essas funções.

Tudo muito bacana. Agora precisamos saber como adicionar um contato novo na lista de contatos. Como vimos, uma lista é uma tripa de pares em que cada par contém um elemento da lista e (um poonteiro para) o restante da lista. Dado um valor x e uma lista lst, podemos criar um novo parzinho em que o car é x e o cdr é a lista lst, e assim estamos criando uma lista cujo primeiro elemento é x e o restante da lista é a lista lst:

#;1> (define lst (list 1 2 3))
#;2> lst
(1 2 3)
#;3> (cons 5 lst)
(5 1 2 3)

Então, para adicionar um contato novo à lista de contatos, basta fazer um cons do contato novo com a lista existente. O problema é que cons cria uma lista nova; ele não altera a lista original:

#;4> lst
(1 2 3)

O que nós precisamos é atribuir a lista nova à variável contatos. Para isso, usamos o operador especial set!:

#;5> (set! lst (cons 5 lst))
#;6> lst
(5 1 2 3)

Agora já temos todas as ferramentas necessárias para escrever a função inserir-contato. Ela deve perguntar ao usuário o nome e o telefone, criar um contato com esses dados, e adicioná-lo a lista global de contatos:

(define (inserir-contato)
  (define nome (lê-string "Nome: "))
  (define telefone (lê-string "Telefone: "))
  (set! contatos (cons (novo-contato nome telefone) contatos)))

Vamos testar nosso programa?

#;1> ,l agenda.scm
; loading agenda.scm ...

Note: the following toplevel variables are referenced but unbound:

  listar-contatos (in menu)
  procurar-contato (in menu)
  remover-contato (in menu)
  sair (in menu)
#;1> (inserir-contato)
Nome: Hildur
Telefone: 555-1234
#;2> (inserir-contato)
Nome: Magnús
Telefone: 234-5678
#;3> contatos
(("Magnús" "234-5678") ("Hildur" "555-1234"))

Parece um sucesso. Vamos agora fazer a função que lista todos os contatos. Essa função é basicamente um loop que percorre toda a lista, imprimindo cada contato. Como vimos, um loop pode ser implementado como uma função que chama a si mesma. Mas como o loop tem que percorrer a lista, a função deve receber a lista como argumento, imprimir um contato, e repetir o loop (i.e., chamar a si própria) com o restante da lista, até chegar ao fim (i.e., à lista vazia). Vai ficar algo assim:

;; 1. Função que recebe _um_ contato e imprime:
(define (imprime-contato contato)
  (printf "Nome: ~a\n" (nome-do-contato contato))
  (printf "Telefone: ~a\n" (telefone-do-contato contato))
  (printf "\n"))

;; 2. Função que recebe uma lista de contatos e imprime cada um (o loop):
(define (imprime-contatos lst)
  (cond
   ;; Se chegamos ao fim da lista (i.e., à lista vazia), nada a fazer.
   [(null? lst) (void)]
   ;; Senão, imprimimos o primeiro contato e repetimos a função para o restante da lista.
   [else (imprime-contato (car lst))
         (imprime-contatos (cdr lst))]))

;; 3. Finalmente, função que imprime a lista global de contatos (chamada a partir do menu):
(define (listar-contatos)
  (imprime-contatos contatos))

O (void) não é estritamente necessário; ele é só uma maneira de dizer "não faça nada e não retorne valor nenhum". Ao invés dele, poderíamos ter retornar um valor qualquer, como #f, já que a função imprime-contatos é chamada apenas para imprimir os valores da lista, mas não se espera que ela retorne nenhum valor particularmente útil.

Na verdade, o Scheme tem uma função pronta, for-each, que recebe uma função e uma lista e chama a função especificada sobre cada elemento da lista. Então nós não precisaríamos escrever a função imprime-contatos, e a nossa função listar-contatos ficaria:

(define (listar-contatos)
  (for-each imprime-contato contatos))

Mas eu precisava mostrar como iterar sobre uma lista no geral, so there we go.

A próxima função é a de procurar um contato por nome. Ela deve perguntar um nome ao usuário e iterar sobre a lista de contatos até encontrar algum contato com o nome especificado, ou chegar ao fim da lista e descobrir que não havia nenhum contato com aquele nome. Primeiro, vamos escrever uma função que recebe um nome e uma lista de contatos e procura um contato com aquele nome:

(define (procura-contato nome contatos)
  (cond
   ;; Se chegamos na lista vazia, é porque não achamos nenhum
   ;; contato com o nome procurado.  Nesse caso, retornaremos #f.
   [(null? contatos) #f]
   ;; Caso contrário, olhamos para o primeiro elemento da lista.
   ;; Se ele tiver o nome que estamos procurando, retornamos o contato.
   [(string=? nome (nome-do-contato (car contatos)))  (car contatos)]
   ;; Caso contrário, procuramos no restante da lista.
   [else (procura-contato nome (cdr contatos))]))

De posse dessa função, podemos escrever a que pergunta um nome ao usuário, procura-o na lista global, e imprime o contato, se existir, ou uma mensagem de erro, caso contráro.

(define (procurar-contato)
  (define nome (lê-string "Nome procurado: "))
  (define contato (procura-contato nome contatos))
  (cond
   [contato (imprime-contato contato)]
   [else (display "Contato não encontrado!\n")]))

Note que a variável local contato vai conter ou um contato ou #f, dependendo de o contato existir ou não. Assim como em C o valor 0 é considerado falso e qualquer outro valor é considerado verdadeiro, em Scheme o valor #f é falso e qualquer outro valor é considerado verdadeiro. Assim, a primeira cláusula do cond acima será executada se contato não for #f, i.e., se um contato foi encontrado pela função procura-contato.

Vamos agora à função de remoção de contato. Para isso, escreveremos uma função que recebe uma lista de contatos e um nome a ser removido, e retorna uma nova lista de contatos sem o contato com aquele nome. Essa função apresenta uma novidade: é a primeira função que escrevemos que produz uma lista como resultado. Vamos ver como vamos fazer isso.

(define (remove-contato nome contatos)
  (cond

Se a lista de contatos está vazia, não há nada a remover; podemos simplesmente retornar a própria lista de contatos vazia.

   [(null? contatos) contatos]

Caso contrário, olhamos para o primeiro contato na lista. Se o nome dele for igual ao que estamos procurando, para removê-lo basta devolvermos a lista sem o primeiro elemento:

   [(string=? nome (nome-do-contato (car contatos)))  (cdr contatos)]

Finalmente, o caso mais complicado. Se o primeiro elemento da lista não é o que estamos procurando, ele vai continuar sendo o primeiro elemento da lista nova, certo? Mas o restante da lista nova vai ser igual ao restante da lista antiga depois de removido o elemento que estamos procurando, usando a própria função remove-contato:

   ;     ,--- Criamos uma lista
   ;     |
   ;     |     ,-- onde o primeiro elemento é o mesmo da original
   ;     |     |
   ;     |     |              ,-- e o restante é o restante da original
   ;     |     |              |   depois de aplicada a rotina de remoção
   ;     V     V              V
   [else (cons (car contatos) (remove-contato nome (cdr contatos)))]))

Com isso, podemos escrever a função que pergunta um nome e remove o contato da lista global:

(define (remover-contato)
  (define nome (lê-string "Nome a remover: "))
  (set! contatos (remove-contato nome contatos)))

Falta implementar a opção de sair do programa. Na verdade, o nosso menu atualmente só executa uma vez, então tecnicamente o programa sempre "sai" depois de executar qualquer opção. O que nós queremos é (1) fazer o programa executar o menu em loop; e (2) uma maneira de quebrar o loop quando o usuário seleciona a opção 5. Uma maneira de fazer isso é fazer o menu chamar a si mesmo em todas as cláusulas do case menos na cláusula da opção 5, o que é meio tosco porque temos que escrever a re-chamada quatro vezes. Outra opção é colocar um condicional no final da menu que faz um "se a opção for diferente de 5, chama (menu)". O melhor talvez seria usar uma saída não-local, mas isso daria spoiler de posts futuros. O que eu vou fazer é o seguinte: menu executa uma única vez, mas retorna #t se o programa deve continuar executando e #f se ele deve parar. Aí então escrevemos uma função main-loop que chama menu e decide se rechama a si mesma dependendo do que a main retornar. A função menu fica:

(define (menu)
  (display "=== Menu ===
  (1) Inserir contato
  (2) Listar todos os contatos
  (3) Procurar contato por nome
  (4) Remover contato
  (5) Sair\n")
  (case (lê-número "Digite uma opção: ")
    [(1) (inserir-contato) #t]
    [(2) (listar-contatos) #t]
    [(3) (procurar-contato) #t]
    [(4) (remover-contato) #t]
    [(5) #f]
    [else (display "Opção inválida!\n") #t]))

E a main-loop, então, fica:

(define (main-loop)
  (cond [(menu) (main-loop)]
        [else (void)]))

E lá no finalzinho do nosso programa, nós colocamos uma chamada à main-loop:

(main-loop)

Whoa! Está pronto! Agora você pode rodar o programa no interpretador, ou até compilá-lo (lembrando de pôr um (use extras) no começo, no caso do Chicken), e testá-lo a gosto.

Salvando os contatos

O único (é, o único) drawback do nosso programa é que ele perde a memória quando termina. O ideal seria que ele salvasse os contatos em um arquivo ao sair, e os recuperasse ao iniciar. Felizmente, há uma maneira simples de fazer isso. Como sabemos, quando pedimos para ver a lista de contatos no prompt interativo, ela é impressa com aquela notaçãozinha maneira com os elementos entre parênteses. Na verdade, o que o prompt de avaliação faz é ler uma expressão, avaliá-la, e imprimir o resultado usando a função write. write recebe como argumento um valor que se queira imprimir e, opcionalmente, uma porta. Uma porta é como se fosse um FILE * do C, i.e., ela representa (normalmente) um arquivo aberto. O que nós podemos fazer, então, é abrir um arquivo para escrita e usar write para escrever a lista de contatos no arquivo. Posteriormente, podemos abrir o mesmo arquivo para leitura e usar a função read, que lê a representação textual de um dado do Scheme e retorna o dado correspondente.

(define arquivo-de-contatos "contatos.txt")

(define (salva-contatos)
  (let ([porta (open-output-file arquivo-de-contatos)])
    (write contatos porta)
    (close-output-port porta)))
  
(define (carrega-contatos)
  (cond [(file-exists? arquivo-de-contatos)
         (let ([porta (open-input-file arquivo-de-contatos)])
           (set! contatos (read porta))
           (close-input-port porta))]
        [else
         ;; Arquivo não existe; não faz nada.
         (void)]))

E no final do programa, ao invés de só chamar main-loop, fazemos:

(carrega-contatos)
(main-loop)
(salva-contatos)

Agora, se você testar o programa, deverá observar que os contatos são preservados entre execuções. Se você abrir o arquivo contatos.txt, verá que o conteúdo é simplesmente a lista contatos escrita no mesmo formato que o prompt de avaliação usa.

Closing remarks

Um monte de coisas nesse código poderiam ter sido simplificadas se eu tivesse usado funções prontas do Scheme R5RS e da SRFI 1 para manipulação de listas, tais como assoc para procurar um elemento na nossa lista de contatos, ou delete para remover um elemento da lista. Porém, como o objetivo do post era demonstrar como trabalhar com listas em geral, eu acabei preferindo fazer as coisas manualmente. Da mesma forma, existem maneiras melhores de abrir e trabalhar com arquivos, mas eu teria que explicar conceitos ainda não vistos e que iam aumentar muito o tamanho do post (que já saiu bem maior do que o planejado). O código também ficou com uma porção de conds que eu normalmente escreveria usando outras formas, mas eu quis evitar introduzir muitas novidades não relacionadas a listas neste post. Tudo isso será revisto em tempo (eu espero).

No próximo post, deveremos abordar o famoso lambda e entrar mais na parte de programação funcional, e/ou ver mais algumas features sortidas da linguagem. Veremos. Comentários, como sempre, são bem-vindos.

Você pode baixar o código desenvolvido neste post aqui.

9 comentários / comments

Main menu

Posts recentes

Comentários recentes

Tags

em-portugues (213) comp (133) prog (65) life (46) in-english (44) unix (33) pldesign (32) lang (31) random (28) about (26) mind (24) lisp (23) mundane (22) web (17) fenius (17) ramble (16) img (13) rant (12) hel (12) scheme (10) privacy (10) freedom (8) academia (7) esperanto (7) copyright (7) music (7) bash (7) lash (7) home (6) mestrado (6) shell (6) conlang (5) misc (5) emacs (4) politics (4) book (4) php (4) worldly (4) editor (4) latex (4) etymology (4) android (4) film (3) kbd (3) security (3) network (3) wrong (3) c (3) tour-de-scheme (3) poem (2) philosophy (2) comic (2) llvm (2) cook (2) treta (2) lows (2) physics (2) perl (1) german (1) wm (1) en-esperanto (1) audio (1) translation (1) old-chinese (1) kindle (1) pointless (1)

Elsewhere

Quod vide


Copyright © 2010-2019 Vítor De Araújo
O conteúdo deste blog, a menos que de outra forma especificado, pode ser utilizado segundo os termos da licença Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Powered by Blognir.