Elmord's Magic Valley

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

Tour de Scheme, parte 1: Tipos básicos, expressões de vários sabores, e um jogo de adivinhações

2016-04-06 03:06 -0300. Tags: comp, prog, lisp, scheme, tour-de-scheme, em-portugues

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

No último episódio, vimos como compilar e rodar programas em Scheme usando o Chicken. Neste post, veremos algumas construções de Scheme e as diferenças em relação às construções equivalentes nas linguagens HtDP. Em seguida, veremos como escrever um programinha clássico de "adivinhe o número" em Scheme.

[Screenshot do programa de adivinhar o número]
Cores por cortesia do shell-mode do Emacs

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".

Tipos básicos

Você deve lembrar dos tipos de dados básicos das linguagens HtDP (se não lembrar tudo bem, pois nós vamos revê-los agora):

Se você entrar algum desses valores em um prompt de avaliação, o resultado será o próprio valor:

#;1> -6.25
-6.25
#;2> "Hello, world!\n"
"Hello, world!\n"
#;3> #\h
#\h
#;4> #t
#t
#;5> 'foo
foo

Hmm, aqui temos uma curiosidade: diferente do que acontecia nas linguagens HtDP, 'foo produz foo sem o apóstrofe. Vamos fazer uma nota mental desse detalhe e seguir adiante.

Expressões compostas

Nem só de expressões constantes vive o ser humano. Um programa em Scheme consiste majoritariamente de chamadas de função e formas especiais. Chamadas de função são escritas na forma (função argumento1 argumento2...), i.e., a função e os argumentos separados por espaços e envoltos em parênteses. Por exemplo:

#;1> (display "Hello, world!\n")
Hello, world!
#;2> (sqrt 16)
4.0
#;3> (expt 5 3)
125

Operadores aritméticos em Scheme são funções comuns, e são usados exatamente como as demais funções, i.e., se escreve (operador argumentos...):

#;1> (+ 2 3)
5
#;2> (* 2 3)
6

Vale notar que essas operações aceitam um número arbitrário de argumentos:

#;1> (+ 1 2 3 4)
10

Naturalmente, você pode usar o resultado de uma chamada de função como argumento para outra chamada:

#;2> (expt (+ 1 2 3 4) 2)
100
#;3> (* 10 (sqrt 16))
40.0

Operadores de comparação também são funções:

#;1> (< 2 3)
#t
#;2> (> 2 3)
#f

Formas especiais são escritas da mesma maneira, mas usando um operador especial ao invés de uma função. Exemplos de operadores especiais são:

A diferença entre uma chamada de função e uma forma especial é que em uma chamada de função, todos os argumentos são avaliados como expressões, antes mesmo de a função começar a executar, enquanto em uma forma especial, os argumentos não são necessariamente todos avaliados ou considerados como expressões. (No caso do if e cond, certos argumentos só são avaliados dependendo dos valores das condições. No caso do define, o nome da variável ou função simplesmente não é avaliado: (define valor 5) não tenta avaliar valor, mas sim toma-o literalmente como o nome da variável.)

Exemplo: Adivinhe o número

Vamos exercitar um pouco do que vimos até agora escrevendo um programinha clássico que sorteia um número de 1 a 100 e fica pedindo ao usuário que tente adivinhar o número até o usuário acertar ou morrer de tédio. Quando o usuário erra, informamos se o seu chute foi muito alto ou muito baixo. Para isso, vamos precisar de umas funçõezinhas extra ainda não vistas:

Ok. Vamos começar escrevendo uma função para perguntar um número ao usuário e ler a resposta:

(define (pergunta-número)
  (display "Digite um número: ")
  (read-line))

Salve essa função em um arquivo adivinha.scm e carregue-o no interpretador:

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

Agora, podemos testar a função:

#;1> (pergunta-número)
Digite um número: 13
"13"

Que sucesso, hein? Só que queremos que a função retorne um número, não uma string (pois precisamos comparar o número digitado com a resposta certa mais tarde). Vamos modificar a pergunta-número para converter o resultado para número antes de retornar:

(define (pergunta-número)
  (display "Digite um número: ")
  (string->number (read-line)))

Note que:

Isso é diferente das linguagens HtDP, que só permitem uma expressão no corpo da função.

Ok, enough talk. Vamos recarregar o arquivo no interpretador e testar:

#;2> ,l adivinha.scm
; loading adivinha.scm ...
#;2> (pergunta-número)
Digite um número: 13
13

Buenacho barbaridade. Agora vamos escrever uma função que recebe um número (a resposta certa), lê o chute do usuário, guardando-o em uma variável local, compara-o com a resposta certa, e imprime uma mensagem apropriada:

(define (avalia-tentativa gabarito)
  (define tentativa (pergunta-número))
  (cond [(= tentativa gabarito) (display "Você acertou! Parabéns!\n")]
        [(< tentativa gabarito) (display "Muito baixo!\n")]
        [(> tentativa gabarito) (display "Muito alto!\n")]))

Vamos ver se isso funciona? Vamos chamar a função com 42 como a resposta certa, e responder o prompt com 13:

#;1> ,l adivinha.scm
; loading adivinha.scm ...
#;1> (avalia-tentativa 42)
Digite um número: 13
Muito baixo!
#;2>

Well, funcionou, pelo menos para esse caso. O causo é, todavia, que a gente quer que a função fique repetindo enquanto o usuário não acertar. Há várias maneiras de fazer esse loop, mas a maneira mais simples, usando só o que a gente viu até agora, é fazer a função simplesmente chamar a si mesma se a resposta não é a esperada:

(define (avalia-tentativa gabarito)
  (define tentativa (pergunta-número))
  (cond [(= tentativa gabarito) (display "Você acertou! Parabéns!\n")]
        [(< tentativa gabarito) (display "Muito baixo!\n")
                                (avalia-tentativa gabarito)]
        [(> tentativa gabarito) (display "Muito alto!\n")
                                (avalia-tentativa gabarito)]))

Ok, dava para simplificar várias coisas nesse código (e.g., unificar os dois últimos casos para escrever a re-chamada só uma vez), mas por enquanto está bom assim. (Se você está vindo de uma linguagem não-funcional, você pode estar pensando que é super-ineficiente usar uma chamada de função ao invés de um loop para repetir o corpo. Em Scheme, todavia, essa chamada é tão eficiente quanto um loop, devido a uma feature chamada tail call optimization, que nós vamos deixar para discutir em outra oportunidade.)

Vamos lá testar as esferas do dragão:

#;7> ,l adivinha.scm
; loading adivinha.scm ...
#;7> (avalia-tentativa 42)
Digite um número: 13
Muito baixo!
Digite um número: 81
Muito alto!
Digite um número: 42
Você acertou! Parabéns!
#;8> 

Agora sim. Só falta a parte do programa que gera o número aleatório e chama avalia-tentativa, i.e., falta o equivalente da "main" do nosso programa. Nós podemos escrever o código dentro de uma função main para fins de organização (e de poder testar a função a partir do prompt do interpretador), mas, como mencionado no último post, não existe em Scheme uma função especial "main" por onde a execução começa. O programa simplesmente executa todas as expressões no corpo do programa em seqüência. Então, podemos simplesmente atirar geração do número aleatório e chamada a avalia-tentativa no corpo do programa (lembrado de pôr a chamada depois da definição de avalia-tentativa; caso contrário, a função ainda não vai estar definida quando ocorrer a chamada); ou então podemos colocar isso numa função (again, apenas para fins de organização), mas aí temos que chamar a função no corpo do programa. Algo como:

;; Define a função...
(define (main)
  (avalia-tentativa (+ 1 (random 100))))

;; ...e a chama no corpo do programa, para que ela seja executada quando o programa iniciar.
(main)

Note que chamamos avalia-tentativa com (+ 1 (random 100)), pois (random 100) retorna um número entre 0 e 99, mas queremos um número entre 1 e 100, e para isso somamos 1 ao resultado.

Podemos testar no interpretador, mas agora que está tudo feito, podemos ao invés disso compilar o programa e testar o executável diretamente:

$ csc adivinha.scm
$ ./adivinha 

Error: unbound variable: random

	Call history:

	adivinha.scm:19: main	  
	adivinha.scm:15: random	  	<--

Ei! Que sacanagem é essa? Well, acontece que random é definida em um pacote que é carregado automaticamente pelo interpretador, mas não no código compilado. Isso é um acontecimento relativamente comum no Chicken. Para resolver isso, primeiro precisamos saber qual é o pacote que contém a função random. Para isso, podemos usar o chicken-doc, que vimos como instalar no post anterior:

$ chicken-doc random
Found 2 matches:
(random-bsd random)  (random N)
(extras random)      (random N)

Well, temos duas opções. random-bsd é um egg (que não vem por padrão com o Chicken e você pode instalar com o chicken-install). extras é um pacote/unit/como-preferir que acompanha o Chicken e não requer a instalação de nada especial. É esse que vamos usar. Tudo o que precisamos é adicionar a linha:

(use extras)

no começo do nosso código. Agora, podemos recompilar e testar:

$ csc adivinha.scm
$ ./adivinha 
Digite um número: 32
Muito alto!
Digite um número: 10
Muito baixo!
Digite um número: 20
Muito alto!
Digite um número: 15
Você acertou! Parabéns!
$

Uff! Funcionou. Nosso programa inteiro ficou assim:

(use extras)

(define (pergunta-número)
  (display "Digite um número: ")
  (string->number (read-line)))

(define (avalia-tentativa gabarito)
  (define tentativa (pergunta-número))
  (cond [(= tentativa gabarito) (display "Você acertou! Parabéns!\n")]
        [(< tentativa gabarito) (display "Muito baixo!\n")
                                (avalia-tentativa gabarito)]
        [(> tentativa gabarito) (display "Muito alto!\n")
                                (avalia-tentativa gabarito)]))

(define (main)
  (avalia-tentativa (+ 1 (random 100))))

(main)

Exercício: Experimente modificar o programa para, quando o usuário acertar, mostrar quantas tentativas foram usadas para adivinhar o número. Como conservar essa contagem durante a execução do loop (que na verdade é uma função que chama a si própria)?

Dica: Para imprimir uma mensagem como "Você acertou em N tentativas", você pode simplesmente usar vários displays em seqüência, i.e.:

(display "Você acertou em ")
(display N)
(display " tentativas\n")

Ou, em Chicken e Racket, você pode usar (printf "Você acertou em ~a tentativas" N). printf não é uma função padrão do Scheme R5RS, mas sim uma extensão do Chicken, Racket e possivelemente outras implementações.

Closing remarks

No nosso exemplo, definimos uma função separada para perguntar o número ao usuário, ao invés de fazer a pergunta diretamente na função avalia-tentativa. Eu fiz isso por dois motivos.

O primeiro é que é considerado bom estilo em Scheme definir funções pequenas com um propósito bem definido. Não só porque o código tende a ficar mais legível, mas também porque isso facilita testar separadamente cada parte do programa a partir do prompt de avaliação. Como você pôde observar neste post, o estilo normal de desenvolvimento em Scheme é ir escrevendo o programa aos poucos, recarregando as modificações no prompt de avaliação, e testando à medida em que se escreve. (Nada lhe impede de escrever o programa inteiro e testar depois, mas, mesmo nesses casos, poder chamar as diversas partes do programa individualmente a partir do prompt é útil para debugar o programa e descobrir a origem de um erro.)

Por falar em bom estilo, em Scheme é considerado bom estilo dar nomes descritivos às funções (principalmente) e às variáveis. Acostume-se a dar nomes-descritivos-separados-por-traços às funções (como fizemos neste post), e a usar o recurso de auto-completar do seu editor favorito para não se cansar digitando (Ctrl+N no Vim, Alt+/ no Emacs, configurável em ambos).

O segundo motivo é que, em Scheme R5RS, defines aninhados só podem aparecer no começo do corpo da função (e no começo de algumas outras formas especiais ainda não vistas), i.e., não podemos escrever:

(define (avalia-tentativa gabarito)
  (display "Digite um número: ")
  (define tentativa (string->number (read-line)))
  (cond ...))

pois o define não é a primeira coisa no corpo da função. Diversas implementações de Scheme permitem defines fora do começo da função, incluindo o próprio Chicken, mas os detalhes e o comportamento variam de Scheme para Scheme e podem trazer umas surpresas desagradáveis, e portanto eu prefiro evitá-los. Para definir variáveis fora do começo da função, pode-se usar a forma let, cuja sintaxe é:

(let ([variável1 valor1]
      [variável2 valor2]
      ...)
  corpo no qual as variáveis são visíveis)

Assim, poderíamos escrever:

(define (avalia-tentativa gabarito)
  (display "Digite um número: ")
  (let ([tentativa (string->number (read-line))])
    (cond ...)))

A forma com a função pergunta-número separada é mais legível, anyway.

Aproveito a oportunidade para mencionar que parênteses e colchetes são totalmente intercambiáveis em Chicken, Racket, Guile e provavelmente outras implementações. Você pode usar apenas parênteses, se preferir (em Scheme R5RS puro apenas os parênteses são definidos), mas costuma-se usar colchetes em formas como let e cond para facilitar a leitura.

(close (current-post))

Por hoje ficamos por aqui. Eu pretendia falar sobre listas neste post originalmente, mas vai ficar para o próximo da série. Como sempre, sugestões, comentários, dúvidas, reclamações, etc., são sempre bem-vindos.

Comentários / Comments (11)

Marcus Aurelius, 2016-04-06 16:45:12 -0300 #

FYI: eu segui o tutorial do "adivinhe o número", só para qebrar o gelo com o Chicken e o Emacs qe instalei e estavam sem uso nenhum até agora :-)

(Obs.: sou contra o desperdício de letras, então 2 Us foram economizados na mensagem acima :-) — As letras desta nota não contam)


Vítor De Araújo, 2016-04-07 08:34:48 -0300 #

@Marcus: Que sucesso essa série. Nem vou mais me sentir tão mal de ter escrito esses posts ao invés de trabalhar na minha dissertação. :P

Se o objetivo é economizar letras, acho q́ adotar umas cõvẽções da "Arte da Língoa de Iapam" pode ser mais eficaz. :P


Marcus Aurelius, 2016-04-07 10:39:10 -0300 #

Combining characters... I see what you did there :-p

Eu tinha procurado um q pré-combinado com algum acento para fazer exatamente isso mas não encontrei (e achei qe o suporte a combining characters não fosse bom suficiente).

Mas ǵ pré-combinado existe. Já estou pensando em alguns usos para isso!

(se bem qe o funcionamento do Backspace no Windows com combining characters é muito legal: apaga o acento e depois a letra. Deveria ser o padrão isso)


Kraftwerk, 2016-04-07 14:21:20 -0300 #

As has been the case with all Kraftwerk's releases, 1981's 'Computer World' is a production tour de FRANCE with blanketing sound effects creating an environment compatible with the album's intended mirroring of a computer pervaded-society. Integrating machine-driven rhythms, synthesizer tapestries and perceptive lyrics, Kraftwerk demonstrates why it has been a major musical influence since its 1975 hit 'AUTOGOL'


Vítor De Araújo, 2016-04-07 15:30:01 -0300 #

@Marcus: C-x 8 RET COMBINING ACUTE ACCENT RET :P

Mais um ponto para o Emacs, mas em pleno 2016 era de se esperar que o sistema deixasse digitar acento + qualquer coisa e gerasse a seqüência com o combining character automaticamente se não houvesse um pré-composto. Anyway.

Aparentemente o backspace com combining character no Emacs faz a mesma coisa. Today I learned. O bash/readline apaga o caractere inteiro (letra + acento).

O suporte a combining characters não é 100%, mas até que meio que quase funciona direito. :P No Firefox, ele funciona com algumas fontes (mas como eu uso um stylesheet que força tudo a ficar em Fixed, o acento aparece no caractere seguinte). No Midori ele renderiza o acento no caractere certo mesmo com a Fixed. O XTerm, que é o último programa que eu esperaria que suportasse combining characters direito, suporta sem problemas.

@Kraftwerk: Esse cara me dá orgulho. :P


@@Kraftwerk, 2016-04-08 15:58:45 -0300 #

oBRIGADO


Vítor De Araújo, 2016-04-13 04:34:17 -0300 #

Nota para a galera que está acompanhando a série: O próximo episódio já está na forja, mas vai levar uns dias ainda para sair. Comecei a escrever hoje, mas ainda não decidi a melhor maneira de organizar e explicar os assuntos, e como são 4 da manhã eu resolvi parar por hoje. :P


Marcus Aurelius, 2016-04-13 11:58:32 -0300 #

Nota: no ErgoEmacs a combinação para autocompletar é Ctrl+Alt+/
Já começa a ser possível usar o Emacs a sério sabendo esse comando.

Eu sei q́ usar um editor q́ eu não conheço bem com um keybinding ainda mais desconhecido é pedir para se incomodar, mas ainda assim deve ser melhor do q́ se acostumar com C-w no Emacs normal e sair fechando janelas sem qerer em outros programas por aí... :-)

C-x 8 RET COMBINING ACUTE ACCENT RET

Uau, esse comando é mũitíssimo prático :o)
Ops, desativando a ironia agora q́ eu vi q́ tem autocompletar bem prático com barra de espaço. Digitei tudo por extenso na primeira vez :-|


E funciona no ErgoEmacs também. Só fiqei encucado com o trecho “8 RET” aí. Por q́ o número 8? E se precisa do Enter (RET), quantos outros comandos começanndo com C-x 8 devem existir? Mistério...


PS: Usando q́ como um miguxo de 1604.


Vítor De Araújo, 2016-04-13 14:09:44 -0300 #

Hmm, não sabia que o espaço completava; eu estava usando TAB. Aliás é muito bacaninha só dar TAB sem digitar nada e ver uma lista de todos(?) os nomes de caracteres do Unicode. :P

Quanto à dúvida de quantos comandos devem existir, é só dar C-x 8 C-h e ver (C-h no meio de um comando incompleto lista todas as possibilidades). Todos eles são inserção de caracteres especiais, acho. O 8 deve ser um mnemônico de "8-bit character" (num tempo em que não-ASCII significava usar um ISO-8859-algumacoisa).


Marcus Aurelius, 2016-04-14 13:42:09 -0300 #

Esse mnemônico do 8 realmente vai ajudar. Aos poucos o Emacs vai ficando mais útil para mim.

Isso me deu a ideia de um site/blog [que na prática eu nunca vou criar] de mnemônicos (podendo ou não incluir o histórico de cada coisa).

Estou atualmente tentando decorar a diferença no CSS de white-space: pre-wrap e pre-line. Não é nada intuitivo, pelo nome os dois parecem ser a mesma coisa.

Deve haver outros mnemônicos legais para colocar nesse hipotético blog.


Vítor De Araújo, 2016-04-14 23:27:00 -0300 #

Whoa, fui olhar a descrição do pre-wrap/pre-line. I don't even. :P O site da Mozilla pelo menos tem uma tabelinha maneira explicando a diferença:
https://developer.mozilla.org/en-US/docs/Web/CSS/white-space

Eu apóio essa idéia de blog. Hoje em dia a modinha parece ser usar o Tumblr pra essas coisas. Não sei se tem alguma conveniência em relação a um blog comum. Faz horas que eu penso em criar um pra compartilhar etimologias de palavras que eu descubro, mas também acabo nunca fazendo. :P Também não sei se faz sentido criar um blog/tumblr separado pra isso ou se só posto aqui com uma tag, mas é que no geral seriam posts muito pequenos pra esse blog aqui, acho.


Deixe um comentário / Leave a comment

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.