Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Iniciando em GOLANG (GO) e BrainFuck

Fiz um test-drive em GO, e para isso aprendi um pouco de BrainFuck

Gopher

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

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:

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:

Agora é continuar os estudos.

Leituras recomendadas

Adicionado em 23/MAIO/2018