While I was playing around with a particular problem this week, a
small discovery led me to a major design fix for the solution I had
developed. Here I will tell you this story because it is not only
about coding, but about inspiration, and its many sources.
The problem was the following:
You will have to trace the steps of a robot to determine its final
commands in the following language:
L - turn 90 degrees left
R - turn 90 degrees right
M - move forward
T - transport to a given location
The commands will be given as such:
10 10 # board size
2 5 N # initial location and the direction the robot is facing
LLRRMMMRLRMMM # series of moves
T 1 3 # transport to position x=1 y=3
LLRRMMRMMRM # another series of moves
Summing up, it is a small textual logo.
My approach to the problem was basically to draft the structures I
would need, in a semi object oriented approach (old-habits). Much like
the following(the first version is in portuguese):
(define-struct tabuleiro (x y) #:transparent)
(define-struct robo (x y direção) #:transparent)
(define (constrói-tabuleiro dados)
(let ([dados-do-tabuleiro (string-tokenize dados)])
(make-tabuleiro (string->number (first dados-do-tabuleiro))
(string->number (second dados-do-tabuleiro)))))
(define (constrói-robo dados)
(let ([dados-do-robo (string-tokenize dados)])
(make-robo (string->number (first dados-do-robo))
(string->number (second dados-do-robo))
(string-ref (third dados-do-robo) 0))))
All was going pretty smooth. I had defined some structures, later
the functions that mapped to the commands in the mini logo:
(define DIREÇÕES-POSSIVEIS
(list '(#\N (#\W #\E))
'(#\W (#\S #\N))
'(#\S (#\E #\W))
'(#\E (#\N #\S))))
(define RESULTADO-DO-PASSO-A-FRENTE
(list (list #\N (list (λ (n) n) add1))
(list #\W (list sub1 (λ (n) n)))
(list #\S (list (λ (n) n) sub1))
(list #\E (list add1 (λ (n) n)))))
(define (muda-direção o-robo virar-para)
(let ([possíveis-direções
(second (findf (λ (uma-possibilidade)
(equal? (first uma-possibilidade)
(robo-direção o-robo)))
DIREÇÕES-POSSIVEIS))])
(struct-copy robo
o-robo
(direção (if (equal? virar-para #\L)
(first possíveis-direções)
(second possíveis-direções))))))
(define (muda-posição o-robo x y)
(struct-copy robo
o-robo
(x x)
(y y)))
(define (passo-a-frente o-robo)
(let ([possíveis-funções
(second (findf (λ (um-resultado-de-uma-posição)
(equal? (first um-resultado-de-uma-posição)
(robo-direção o-robo)))
RESULTADO-DO-PASSO-A-FRENTE))])
(muda-posição o-robo
((first possíveis-funções) (robo-x o-robo))
((second possíveis-funções) (robo-y
o-robo)))))
That also went ok. But the problem was parsing the language. I did
my own ad-hoc parser, but as I was building it, the felling of
suspicion was growing and my faith in my approach diminishing. Take
a look at the final product:
(define (ler-movimentos-do-robo arquivo
(o-robo #f)
(o-tabuleiro #f))
(let* ([linha-de-comandos (read-line arquivo 'any)])
(if (eof-object? linha-de-comandos)
o-robo
(let ([elementos-da-linha (string-tokenize linha-de-comandos)])
(case (length elementos-da-linha)
[(3)
(if (equal? (first elementos-da-linha) "T")
(ler-movimentos-do-robo arquivo
(muda-posição
o-robo
(string->number (second
elementos-da-linha))
(string->number (third
elementos-da-linha)))
o-tabuleiro)
(ler-movimentos-do-robo arquivo
(constrói-robo linha-de-comandos)
o-tabuleiro))]
[(2) (ler-movimentos-do-robo arquivo
o-robo
(constrói-tabuleiro linha-de-comandos))]
[else
(ler-movimentos-do-robo arquivo
(string-fold
(lambda (comando velho-robo)
(if (equal? comando #\M)
(passo-a-frente velho-robo)
(muda-direção velho-robo comando)))
o-robo
linha-de-comandos)
o-tabuleiro)])))))
Big, butt-ugly, hard to read and maintain. Something was wrong
and I wasn't really sure what. Then serendipity struck me in the
form of
this message. Lex
and YACC, of course! It took me a couple of hours to learn Racket's
syntax for it, but I ended up with a lexer and a parser for the
mini logo, in a version that was much more friendly and elegant.
(define-tokens value-tokens (NUMBER
DIRECTION
FUNCTION
COMMANDS))
(define-empty-tokens op-tokens (EOF NEWLINE))
(define-lex-abbrevs
(teleport #\T)
(turn-left #\L)
(turn-right #\R)
(move-forward #\M)
(directions (:or #\N
#\S
#\E
#\W))
(digit (:/ #\0 #\9)))
(define mech-lexer
(lexer
[(eof) 'EOF]
[#\newline (token-NEWLINE)]
[#\space (mech-lexer input-port)]
[directions (token-DIRECTION (string-ref lexeme 0))]
[(:+ (:or turn-right turn-left move-forward))
(token-COMMANDS
(map (λ (command)
(get-consequences COMMAND-LOOKUP-TABLE command))
(string->list lexeme)))]
[teleport (token-FUNCTION (string-ref lexeme 0))]
[(:+ digit) (token-NUMBER (string->number lexeme))]))
(define (mech-parser gen
(board #f)
(mech #f))
((parser
(start start)
(end EOF NEWLINE)
(tokens value-tokens op-tokens)
(error (λ (tok-ok? tok-name tok-value)
(error 'lexer
(format
(if (false? tok-ok?)
"the token ~a with value ~a was invalid."
"unknow error with token ~a with value ~a")
tok-name
tok-value))))
(grammar
(start [() mech] [(exp) $1])
(exp [(NUMBER NUMBER)
(mech-parser gen
(make-board $1 $2)
mech)]
[(NUMBER NUMBER DIRECTION)
(mech-parser gen
board
(make-mech $1 $2 $3))]
[(FUNCTION NUMBER NUMBER)
(with-handlers ((string? (λ (message)
(display message)
(mech-parser gen
board
mech))))
(mech-parser gen
board
(jump board mech $2 $3)))]
[(COMMANDS)
(mech-parser gen
board
(foldl
(λ (command old-mech)
(with-handlers ((string? (λ (message)
(display message)
old-mech)))
(command board old-mech)))
mech $1))])))
gen))
(define (process-mech ip)
(mech-parser (λ () (mech-lexer ip))))
If you want to compare the full versions, take a look at the
github repository. The
version with the cleaner Lexer/Parser are called 'mech' instead of
'robo'.
Until next time!