Iniciando em GOLANG (GO) e BrainFuck
Fiz um test-drive em GO, e para isso aprendi um pouco de BrainFuck
Outra linguagem para aprender
Há alguns anos ouço falar em GO. Especialmente por atualmente ter Python como principal linguagem para sobreviver, chega a mim muito material sobre GO através da comunidade Python.
Embora prático, eficiente e com um mercado de trabalho bem abrangente, o mundo das linguagens dinâmicas não me apetece mais como antes. Por isso, estava ensaiando uma volta à programação C, e com isso, cheguei ao Rust.
Mas, embora sendo nada de mais grave, também me incomoda na comunidade Rust o fato de muita coisa ainda depender do compilador gerado no dia, com o que tem na master
, os nightly builds.
Enfim, quero compilar as coisas, não quero ter que instalar ambientes virtuais e ficar instalando dependências a cada deploy, alem é claro, de ter ferramentas estáveis e com uma abrangente biblioteca padrão.
Mas eis que esses dias percebi um pequeno detalhe sobre a linguagem GO. Um de seus idealizadores é nada mais nada menos que Ken Thompson, um dos mentores do UNIX, e da linguagem precursora do C, entre outras coisas.
Pois é, tive que ceder a esse "apelo à autoridade", e gostei.
E cá estou eu tentando entender como é a linguagem.
Pensando no que fazer pra estudá-la, lembrei do BrainFuck. Inspirei-me nela e implementei um conjunto reduzido de seus comandos (a linguagem original tem 8).
Assim ficou o meu primeiro programa em GO:
package main
import (
"bufio"
"fmt"
"log"
"os"
)
type Language struct {
source []rune
tokens map[rune]func(*Language)
memory [100]int
pos int
}
func (l *Language) Execute() {
for _, token := range l.source {
if function, ok := l.tokens[token]; ok {
function(l)
} else {
log.Fatalf("Invalid token [%c]\n", token)
}
}
}
func (l *Language) AddToken(t rune, f func(*Language)) {
if l.tokens == nil {
l.tokens = make(map[rune]func(*Language))
}
l.tokens[t] = f
}
func (l *Language) AddToSource(line string) {
for _, c := range line {
if _, ok := l.tokens[c]; ok {
l.source = append(l.source, c)
}
}
}
func main() {
var program Language
var tokens = map[rune]func(*Language){
'>': func(l *Language) { l.pos++ },
'<': func(l *Language) { l.pos-- },
'+': func(l *Language) { l.memory[l.pos]++ },
'-': func(l *Language) { l.memory[l.pos]-- },
'.': func(l *Language) { fmt.Println(l.memory[l.pos]) },
}
for t, f := range tokens {
(&program).AddToken(t, f)
}
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
(&program).AddToSource(line)
}
(&program).Execute()
}
Vamos aos detalhes desse código.
Cabeçalho
A primeiro linha - package main
- declara o nome do pacote. O nome main
é especial, e deve ser usado quando estamos trabalhando em um arquivo único ou em um arquivo que será um executável.
Pacotes
Logo tem-se a listagem dos pacotes usados no programa:
import (
"bufio"
"fmt"
"log"
"os"
)
bufio
Buferização e atalhos para I/o textual.fmt
Entrada e saída formatadas.log
Log básico.os
Serviços do sistema operacional independentes da plataforma.
Se fosse necessário apenas um pacote, poderia-se importá-lo como, por exemplo, em import "fmt"
.
Um tipo que define uma estrutura
type Language struct {
source []rune
tokens map[rune]func(*Language)
memory [100]int
pos int
}
Quando digo a GO type Language struct
, ele entende que quero definir um tipo que seja uma estrutura, cujo nome é Language
.
Usamos alguns tipos nessa estrutura:
- Tipos numéricos:
rune
é um apelido paraint32
e é usado para conter um caractere UNICODE.int
é o tipo inteiro que caracteriza a arquitetura (por exemplo, 32 ou 64 bits)
A declaração source []rune
diz que source
é um array de tipos rune
, de tamanho não definido, e portanto, não pronto para uso.
Já em tokens map[rune]func(*Language)
, definimos que token
será um mapa contento funções (associadas ao tipo que está sendo criado, ou seja, Language
) cada uma identificada por um rune
. O asterisco é o que permitirá a essas funções alterarem dados da estrutura (melhor dizendo, até se altera os dados sem essa "permissão", mas são perdidas ao final da função).
A linha memory [100]int
define um array de tamanho fixo de 100 números inteiros.
A última, e mais fácil, define pos
como um número inteiro.
Função associada que executa o programa
func (l *Language) Execute() {
for _, token := range l.source {
if function, ok := l.tokens[token]; ok {
function(l)
} else {
log.Fatalf("Invalid token [%c]\n", token)
}
}
}
Na primeira linha lemos que uma função está sendo definida (func
), usando a variável l
como referência aos dados, com "poder de escrita" (*
) associada à estrutura Language
, função essa cujo nome é Execute
, e que não recebe parâmetros ()
, e também não retorna nada (pela ausência de informações).
A instrução for
(única forma de repetição em GO), aqui tem uma de suas formas. Em geral, o for
pode ser escrito como se escreve em C, mas nesse caso temos a palavra range
que provê a funcionalidade de retornar elemento a elemento de um map
ou array. No caso de um array (l.source
), é retornado em cada iteração o índice e o valor. Como não precisamos do índice,fazemos uma "atribuição vazia", usando _
para descartar o valor, e a variável token
recebe o elemento da vez.
O if
permite uma construção inicial de atribuição antes de efetivamente fazer a verificação. Nesse caso, estamos verificando se o token
da vez existe em nosso mapa de tokens. Essa verificação também retorna dois valores, nesse caso o valor (function
), se existir, e um valor boleano em ok
, que será verificado logo em seguida.
Caso exista o token
chamamos a função passando a variável l
, que contem a estrutura. Caso não exista (ele
) abortamos o programa com uma mensagem na tela.
Função associada que alimenta o mapa de tokens
func (l *Language) AddToken(t rune, f func(*Language)) {
if l.tokens == nil {
l.tokens = make(map[rune]func(*Language))
}
l.tokens[t] = f
}
Nessa outra função (func
) que usa l
como referência, com poder de escrita (*
), sobre a estrutura Language
cujo nome é AddToken
e que recebe como parâmetros a variável t
do tipo rune
e f
do tipo função (func
) com "poder de escrita" ((*
) associada à estrutura Language
, ufa!, verificamos se tokens
ainda está vazio, para, se for o caso, criarmos um map
com make
, e então passar a adicionar elementos, o que é feito por simples atribuição: l.tokens[t] = f
.
Função associada que recebe o código-fonte e limpa
Também temos outra função associada (não farei a leitura completa), que recebe line
como parâmetro e tem o tipo string
.
func (l *Language) AddToSource(line string) {
for _, c := range line {
if _, ok := l.tokens[c]; ok {
l.source = append(l.source, c)
}
}
}
A novidade é o método append
, usado para adicionar elementos em um array. Seu funcionamento é tão interessante, que vale mais à pena ler diretamente a documentação.
Temos que passar o array como parâmetro, pois pode ocorrer de uma nova matriz ser criada.
Finalmente a execução
func main() {
var program Language
var tokens = map[rune]func(*Language){
'>': func(l *Language) { l.pos++ },
'<': func(l *Language) { l.pos-- },
'+': func(l *Language) { l.memory[l.pos]++ },
'-': func(l *Language) { l.memory[l.pos]-- },
'.': func(l *Language) { fmt.Println(l.memory[l.pos]) },
}
for t, f := range tokens {
(&program).AddToken(t, f)
}
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
(&program).AddToSource(line)
}
(&program).Execute()
}
Finalmente o ponto de entrada. Como estamos no package main
devemos ter uma func main
. Nada demais aqui.
Logo em seguida declaramos (var
) que usaremos a variável program
do tipo Language
.
Também definimos um mapa, com os tokens de nossa linguagem. Veja que é uma mapa com chaves do tipo rune
apontando para elementos do tipo func
, com poder de escrita, associada ao tipo Language
. Usei funções anônimas, para deixar mais clara a associação entre os tokens e suas ações.
Populamos nossa estrutura com os tokens através de um for
.
Também lemos a entrada padrão e jogamos, linha a linha, para nossa estrutura, que filtra o conteúdo que interessa.
Finalmente executamos o programa que recebemos.
Resultados
O objetivo não era, pelo menos até agora :), escrever um interpretador para uma linguagem BrainFuck-like. Considero que fiz uma prova de conceito, e bem sucedida.
A documentação é muito boa, e artigos e respostas a dúvidas pontuais são encontrados com facilidade.
A ausência de orientação a objetos é um alívio! Fico aliviado em ter uma boa opção para fugir de abstrações exageradas.
Opa, faltou um teste manual desse programa:
$ echo '.+.>.++.<.-.' | go run mfbil.go
0
1
0
2
1
0
$ go build mfbil.go
$ echo '.+.>.++.<.-.' | ./mfbil
0
1
0
2
1
0
O programa .+.>.++.<.-.
faz o seguinte:
- imprima o valor do registro atual (0)
- some 1 a esse valor (agora vale 1)
- imprima o valor do registro atual (1)
- vá para o próximo registro (vale zero na inicialização)
- imprima o valor do registro atual (0)
- some 1 a esse valor (agora vale 1)
- some 1 a esse valor (agora vale 2)
- imprima o valor do registro atual (2)
- vá para o registro anterior (volta para o primeiro)
- imprima o valor do registro atual (1)
- subtraia 1 desse valor (agora vale 0)
- imprima o valor do registro atual (0)
Agora é continuar os estudos.
Leituras recomendadas
- Go Lang em inglês
- Go Lang em português
- Blog do projeto
- Semanário em inglês
- Semanário em português
- BrainFuck