[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.
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".
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.
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:
define, que define variáveis e funções novas:
#;1> (define valor 42) #;2> (define (dobro x) ---> (* 2 x)) #;3> (dobro valor) 84
(---> é o prompt de continuação do Chicken, não parte do código.)
if, usado na forma (if condição expr-then expr-else), que avalia a condição e, se for verdadeira, avalia e retorna o valor de expr-then, e caso contrário avalia e retorna o valor de expr-else:
#;1> (if (even? 5) "5 é par" "5 é ímpar") "5 é ímpar" #;2> (if (> 5 0) "5 é maior que zero" "5 não é maior que zero") "5 é maior que zero"
cond, que faz mais ou menos a mesma coisa, mas permite especificar múltiplas condições, que são testadas seqüencialmente:
#;1> (cond ---> [(even? 5) "5 é par"] ---> [(odd? 5) "5 é ímpar"] ---> [else "5 não é nada"]) "5 é ímpar"
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.)
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.
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.
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.
@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
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)
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'
@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
oBRIGADO
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
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.
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).
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.
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.
Copyright © 2010-2023 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.
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)