- Hello, welcome to Coding Challenge: Logo Interpreter,
part 2, I'm a little bit scared of this.
(clears throat) All right, so what's going on?
Where are we, why am I here?
You might remember me from Logo Interpreter Coding Challenge
number 121, look that's me,
I'm almost wearing the same clothes.
This is a challenge where I looked at
the Logo programming language, a language from
designed in 1967, turtle graphics,
you might be familiar with,
I talked a bit more about it in that video.
I encourage you to read about the history
and also to check out the Logo Foundation
which is the foundation that supports a variety
of wonderful initiatives, including Scratch Day.
So check that out but what I did
was I made this Logo interpreter that is able
to interpret simple commands like forward 60,
left 90, forward 100.
So while this implementation of Logo
interpreting these Logo commands
like forward, left, forward, et cetera works,
it's missing crucial components.
And so there are many commands in Logo
and I implemented pen up, pen down, some of these,
but one of the most important ones
that I did not implement is repeat.
So this is a tutorial from a Brown University page
about the history of Logo that I'll also link to
in this video's description,
but what I want to attempt to do is
add the repeat functionality which you would think
would be a really simple thing,
but it's a little bit more complicated than,
well, there are some simple ways probably to do it,
and I will point out, in fact, that when I posted
this code, 16 pull requests came in with suggestions
and techniques for how to implement repeat,
and I would encourage you to go check all these out.
I want to highlight one of them that was pretty interesting
to me, I'm going to look for it, Regex implementation,
is this the one I'm looking for?
Yeah, this one, oh yeah, I love this,
which is just basically using a regular expression
to search for the repeat command
and then just replace the repeat command
in the string itself with what's the thing
to repeat multiple times.
So kind of like unrolling the string recursively
with a regular expression.
Pretty wild implementation.
I'm not going to use that one,
I'm going to try to spin up my own.
So this is my idea.
So, one thing that I think will help
is object oriented programming.
What if I have this idea of a command class?
So I make this idea of a command class
and it has the name of the command.
That could be forward,
left,
picks the pen up, et cetera.
It has an argument
so if the command is forward,
maybe I would have an argument like 100.
And by the way, the command could be repeat
and I know I'm about to leave your view here.
Repeat and then the argument could be a number also
'cause you're going to repeat something
a certain number of times.
So you have this idea of a command class.
And then maybe what I think would be useful
is to create a parser object.
And actually, instead of using split,
so I used split as a way of looking at the Logo instructions
and just splitting it into a big array,
but I think what I want to try to do
in the most absurd thing ever is just actually step through
the string one character at a time so that I have
the idea of a parser is keeping track of a index.
So the parser is always has basically a pointer
into the command and can step through
and if it finds repeat, it can then pull out a chunk,
it can do all sorts of stuff.
So, if what I'm kind of thinking about here
is this idea of a command.
A command could just be a thing that you execute
with an argument but if the command is repeat,
then it could also have a list of,
it could have an array of its own commands,
that those are the things you repeat.
And then inside of this array, there could be a command
that is a repeat command so it could nested and recursive.
So this is my idea, I have not actually,
I thought about this quite a bit
and probably helped by looking at everybody's pull requests
so I don't know to what extent this is an original idea
or it just came from suggestions,
but it through osmosis, my brain has come up
with this idea but I have not tried to implement it
so what you are about to watch is me actually trying
to implement this for the first time,
and it might be a terrible idea and things will go wrong
and you'll probably notice this video is 342 hours long,
but if you want to stick with me, here we go.
Okay, so let me go to the code.
I'm going to keep the code that I had before,
I'm going to add a new JavaScript file called parser.
Maybe it should be interpreter,
I don't know why I'm going to call it parser.
Parser.js, I'm going to write a parser class
and the idea is that when you create the parser,
you give it some text.
So this is the full text that needs to parse
and then also it has an index which starts at 0, okay.
And, one of the functions that I would have here
is nextToken so it could maybe look and sort of find
the next token or something
in this text by stepping through it with the index, okay.
That's what I'm thinking about.
Now, I am also going to then create another JavaScript class
called command, and this would be,
this is what I'm talking about,
it would have a name and an argument.
And then it could also have potentially subcommands.
Recursively, a command could also include a list
of other commands.
So this is my idea, now, let's go to sketch.js
and basically,
this go turtle function, I'm actually going to just
basically take out what I did before
'cause I don't think this, I'm not going to use split anymore
and I'm not going to have my own index here.
I'm going to say let parser = new parser with that code.
So let's just for example say console.log
and parser.nextToken.
So let me at least see if I can get parser.nextToken
to work, and so...
And this should probably return the token
so return "test".
So if I just run this now,
parser's not defined, right, I always forget this.
I've added a bunch of new JavaScript classes
so I want to make them separate, reference those files here.
Probably should get into bundling and building
and all that sort of stuff at some point.
Parser and command.
Let me go back to sketch, refresh, okay, so, great,
I see test there.
I have got my command but I didn't really get the token.
What I need to do is here, I really need to say all right,
so I'm going to step through the text one character at a time.
So what I mean by that is I'm going to say let char = this.text
charAt
this.index, okay.
So, while the character is not a,
I guess I could use a regular expression here.
So, I want to...
Let me just say, let me just do it this way for a second.
While the character is not a space.
What I want to do is I'm going to start with an empty token,
and I am going to say,
oh I don't need a...
While the character is not a space, token += the character,
character = this.text.charAt,
charAt ++this.index.
So what's going on here?
The idea here and, again, I could use regular expression
and substring but I'm trying to do this in a very manual way
to understand it.
What I'm doing is I'm saying let me look
at the first character and as long as that character
is not a space, I'm going to add that character
to my token and then look at the next character.
And this is basically, remember, this is something
I covered in the first part of the challenge.
This.index ++ this.index is increasing the index by 1
and giving back that new value.
So now if I say return token,
let's see what happens.
Refresh.
This text chatAt, charAt, a little typo there.
There we go, look.
(bell dings)
I got the first token, that's great.
Now, what if I now want to say
here
next token?
Ooh, it got a space, so look at this,
a space is not a valid token so what I can actually do here,
back into parser, is I can actually say if,
if char = a space,
return next,
increase the index, go to the next one
and return this.nextToken.
So this is a way of basically skipping and, again,
this is definitely a clear example
of I'll refactor this later,
I need a little theme music there for that,
but,
this is a start, okay,
so this is basically saying ignore
the space and so now,
we can say I've got forward 60,
but here's what I want to do really.
Here, I want to say...
And let me actually, let me use a regular expression here.
So I'm going to say,
there's a category of command which is like
a movement command, forward, backward, right, left,
so I'm going to say that would be a regular expression
that is forward or backward, this is fb followed by d
or left or right followed by a t
and probably, I should probably also make sure
it's the full string,
so put this beginning and end.
So now I could say
let token = parser.nextToken
if movement
test(token)
so if it's a movement command,
then create a new command
with token and token and parser.nextToken.
Right, 'cause I think I said the command is,
I said the name of the command and the argument
and the argument always comes next.
So, again, I'm not doing a lot of error checking
for people making mistakes in their Logo code
but we'll get to that later, I suppose.
Okay, I'm setting myself up for hopefully being able
to do a repeat.
Where am I here?
So let's just see and actually let's make
an array of commands, I mean I might want to put
all the commands into an array or I might want
to process them one at a time, there's various ways
of doing this but let's actually put it
into an array right now, I think that'll be helpful.
Oops, okay, so let's see and now let's look at commands.
Okay, refresh, look at that, I've got a command
with arguments, the name is forward, the argument is 60
and it doesn't have any subcommands
because it's not a repeat.
Woo, I like this so far.
So now, hmm, this is where I'm going to get into trouble.
Let's do something like while parser
is not empty, while parser has remainingTokens,
I don't know, come up with a better name for that.
While parser has remainingTokens, keep doing this.
So what does it mean here in the parser
to say remeainingTokens, well, on the one hand,
I can just return this.index is less than this.text.length
because if the index has moved past the end of the text,
then there's nothing left.
Hmm.
I'm wondering if this is going,
I bet you I'm going to get an infinite loop here
but let's just give it a try anyway.
Yes.
So I have an infinite loop, I could tell
because everything went blank, this is just there,
so I'm going to actually, unfortunately, have to
kill this page, whoops, ah.
Next token, there are situations where if it hits
the bracket, bracket is not a space, bracket is not a...
So, ah, I guess what I could do here
is if char, I mean this is really terrible
or
if it's one of these brackets,
then,
return, then also go up by an index
and return that character.
So this definitely accounts for another thing.
So, again, this could be condensed and I could maybe use
some more clever ways of doing this
but if it's a space, ignore.
If it's a bracket, whoops, if it's...
And actually, I'm going to want to do something for it's
a repeat, but anyway, if it's a bracket, send that back.
And,
otherwise, accumulate
until a space or bracket,
but, okay, there's so many problems with this
but I have to build it one step at a time.
If you're willing to stick with me
and keep watching this video, then more power to you, okay.
Let's see if I manage to not have an infinite loop.
Still infinite loop.
While character's not a space, keep going up
and this could get stuck.
Or, while character space and this.index
is less than this.text.length.
So I've also got to check to make sure I'm not hitting
the end, oh there we go, okay (laughing).
All right, so that was definitely the problem.
Now at least my infinite loop is gone.
Let's look at these commands.
So forward 60, right 120, so this is good.
This is what I wanted it to be.
But I need to be able to account for the repeat.
So this is good, this is giving me a list of commands
but what if
the token
that I get back, now I need to deal with repeat.
So, if it is a movement,
I should also have something
for a pen command which would really just be matching
a p at the beginning, right, any of the pen commands
match just a p so I should say else if
pen
test token,
so a pen command
is just a command with just the token, no argument.
And now I could, in theory,
if I added a pen up command, we would see
pen up is in there with no argument but that's fine,
so that's good, so this would work, oh why isn't it,
oh it's not drawing anything 'cause I'm not bothering
to draw, sorry, I keep opening up terminal by accident.
Okay, so I have now written a parser that can go
from token to token and can create commands and knows how
to handle any movement command or any pen command.
Oh and this.remaining tokens.
Ah, yes, great suggestion from the chat.
Abdillah Baghat suggests that here, I like this suggestion,
that this is already a test which is this.remaining,
remaining tokens, 'cause I have that function, right,
that already tests that here.
Great, okay, still working, all right.
Now it's time to deal with the repeat.
So I need to handle the repeat in a very unique way
and this is where I'm really going to get lost.
So repeat is just matching the repeat command.
So this could be a regular expression here.
So else if repeat
test token.
So now, what if I get a repeat command?
Then what I need to do is say,
so let command is a new command with the token.
Oh, and also the next token.
So I first get the number,
then what I need to do is I need to pull out, hmm,
I need to pull out everything within the brackets
so then what I want to say is the to repeat is the parser,
get repeat.
So I want to write a new function that then in the parser
looks for that starting bracket, looks for the end bracket
and just gives everything back (claps), okay.
So, in the parser now, I'm going to say getRepeat.
So I should first, basically find that first bracket.
So I want to, in theory, there could be spaces,
so char = this.text, this is really hard,
the way that I've gotten to this text.charAt this.index.
I should probably put that in a function
and then if while char,
I guess I could say, you know what, I could do this.
While this is
not equal to
this opening bracket,
while it's not equal to this opening bracket,
oh ++, right, I want to go ahead to the next one
but stick where I am, then just keep going,
and, and I guess need to make sure,
and
this.remainingTokens.
Now, I have found...
Basically I have found...
I'm going to say index, I'm just going to say
this.index.
This should console.log where the index is.
So I need to put a repeat in here.
So let's start with something really simple
like repeat three this.
And let's refresh, all right.
It got forward, it got right
and it got, ooh weird,
and it said 10.
Is that
0 1 2 3 4 5 6
7 8 9,
oh great,
so now it's at index 10, oh, perfect.
So now, ah, I love this.
Okay, so.
Commands push.
So this is actually a function,
I got to make a subparser of the sub thing.
Ooh.
Ah, yikes, I mean the recursion aspect of this, right,
because a repeat could have a repeat inside of it.
If a repeat couldn't have a repeat inside of it,
I'd kind of be done, maybe I should do that first.
This is going to be a really long video.
I know I keep saying that but it just is.
So now what I want is to
find the last one.
So...
Let's start = this.index.
Let end, now I need to do this again, move along
here,
and let end = this.index and then return this.text
substring start, end.
Let's see if this does what I think it should do.
And then,
let's see if it does what I think it should do.
Okay.
Command,
token,
whew, parser, nextToken,
let's just console.log this.
Console.log to repeat, okay.
Yes, oh okay, look at that, this is the thing to repeat
and the command is the repeat, perfect, ah, yes, okay,
but minus one, minus one, minus one.
So here, in parser, end minus one 'cause the index
is actually the index past that final bracket.
So now,
there we go.
The command is repeat, it's named repeat
and now I just need to fill the commands with an array
of the stuff that's inside of that to repeat.
So this, ah,
okay, I have an idea now.
This function...
Parse,
let commands.
I'm trying to think here, parse.
I need to write a recursive function now
where I come back and do this, then for those subcommands,
okay so let commands equal, this is going to be weird,
let me just try to do parse,
parser.parse,
is this what I want to let commands = parser.parse,
should all this be in the parser?
And then,
this would,
I don't know about putting this in the parser.
If this is in the parser,
parse, oh this is hard.
Okay.
While this
remaining tokens, next, so this I'm not so sure
about putting this in here but I'm going to.
So everywhere I said parser, I'm now saying this.
And then, I am saying let commands is be an array
and then basically, at the end here, I'm going to say
return commands, just give me a second,
I'm not going to do the recursion yet.
So let's see, let's see what happens here.
And then in Sketch, let commands = parser.parse,
console.log
commands.
This is crazy.
All right, sketch.
All right, that's good, that's good, hold on, hold on,
hold on, I'm thinking, I'm thinking,
oh there's a console.log here.
Where is there extra console.log?
Uh, it's 25 and 31.
Oh I have it twice, okay.
Okay, so now,
okay, okay, now, now, this is the hard part.
All right, so I have this to repeat
and then what I want to say is command.commands
equals (gasps)
parse,
this.parse to, this is what I want to do,
to repeat.
Okay.
So this isn't exactly going to work,
this is the idea of what I want to do.
But it's not right yet.
So close, it's not right because the parse command inside,
it's working with this particular,
the sort of global block of text.
Maybe it makes more sense for that not to be,
oh wait, no, no, no, I think I have an idea (gasps).
All I have to do is make a new parser here.
With the to repeat stuff and then
that equals
parser.parse, right?
Because then it would have, I think this might work.
Could this really work?
Let's think about this.
I'm basically saying hey, make a parser with all this code,
parse it and give me the commands.
The parser is going to do that and return that array
of commands but internally, oh, wait, wait, wait, wait,
yeah, it's going to make.
I think this, I mean, it's so hard to speak this.
Let's run it, let's run it.
This is going to produce a lot of errors or an infinite loop,
for sure. (drum roll)
(buzzer buzzing)
Cannot read property length of undefined parser.js line 7.
(vocalizing)
This.text, oh whoops, I took that out
when I was messing around with this idea.
Okay, not playing the drums but okay, command, repeat,
ah!
Ohhh.
(clapping)
I think that worked, okay, so I think this is a case
where I was figuring it out, trying to explain it,
all at the same time but I don't know
that I did that super successfully
so let me take a minute
to try to unpack what just happened.
Okay, and the idea here is that I have something like this,
repeat three,
forward 60, can you see that?
Yes, so the parser finds the first token
and makes a command
repeat
with three.
Then the parser, because it found a repeat,
asks for this as basically a substring
and it makes a new parser object with only this.
So the first parser object started with everything,
the new parser object just works on this
and makes a command.
So I really should put this in a json way.
So the idea here, and let's add some other stuff.
The way that it should work,
imagine if this had forward 60 here.
So the idea is that I would have an array
with commands and the first command would have name forward,
argument 60, then the next command would have name,
right,
repeat,
argument three but it knows it's a repeat,
it then goes and makes a parser for this
and the same way that this is a big array,
the parser now works on this to put it in an array here
with objects like forward 60, et cetera.
So in theory, this could even have another repeat inside
and that repeat would be in this array
with its own subcommands.
Hopefully that helps a little bit more.
But let's actually test this theory.
So let me go now to, let me, I'm afraid to type stuff
in here and break so I think what I'm going to do is just,
so let's start with
forward 60, repeat, let's just start with that first.
So we can see we got a forward
60, no commands and a second command that says repeat three
with two other commands.
Okay.
Now what if I then put in here,
let me do it out here 'cause,
what if I do another repeat forward, oh so repeat four,
forward 10.
Let's look at this.
Okay, so.
There's still forward 60, then there's a repeat
and the repeat argument is three, there are three commands,
one of which is a repeat which has a subcommands in it.
(bell dings) (train whistle)
All right, I think this works, I mean I'm sure
this needs error handling but the basic idea
is actually working, wow this is very exciting to me.
All right, so what I need to do now
is to follow the instructions.
This should be, in theory, this should be the easy part.
So now that I have this commands, I can just say
for let command of commands
and now I forgot how I did this in the first place
'cause I have this, oh boy, this is bad,
I have a,
I have this dictionary thing that I made
to execute these different functions, hmm,
so what do I want to do, this is called commands,
so I'm going to call this command dictionary.
Well I'll just call this commands, command list.
Command list, it's not a list.
Command lookup.
Command look up, boy that's a awkward variable name.
So what I want to do is now I want to say the name
is command.name and the argument is command.argument
and then I'm going to say,
I have to figure out if it's a repeat.
If name = repeat,
then for let i = 0, i is less than arg,
i++, right, 'cause I want to do whatever is,
and then I'm going to go through its commands.
I guess I could say, oh boy, oh, ah,
I need another recursive function to traverse it.
Aah.
Let me go back to something simpler.
Without anything nested.
Forward 60, repeat three times, okay so this idea
works.
But...
This needs to be a function, I need more recursion
or maybe I should be just doing this as I'm going.
So I'm going to call this execute
and executes a list of commands.
So if you execute a list of commands
and then you get the repeat, you need to execute,
command.commands.
Right, so in other words, I'm looking at the commands.
If it is a repeat, then I want to execute the sub commands
that are in these sub commands list
a certain number of times.
Otherwise, I can now say basically what I did
in the very first version of this challenge
which is to do what?
Command list name
arg.
So the idea is I'm looking up the command list,
in the command list, by the name with an argument.
So that will then go here and say if it's forward,
it will execute this function,
moving the turtle forward by an amount.
I think there's a lot of redundant extra code now
but this is fine.
Let's see
what happens.
Syntax error.
Oh this has to say function.
I'm not in an object anymore.
All right, mm, so, hmm,
what is that 100 100, so what's, oh,
oh I didn't execute anything.
Execute commands.
Command list is not defined.
Command lookup, okay, I can't remember what I call anything,
command lookup.
Hey!
(clapping) (bell dings)
This is working.
Wow, okay.
Dare I say this is complete?
Does anybody have access to the code
that produces this design?
I'm going to take a break and try to find it.
(train whistle) Okay, I'm back.
What you don't realize is about an hour passed
of me doing some horrific debugging
and if you want to find that, I will link
to the livestream version of this edited challenge
in the
video description, the thing that you're watching right now,
which is not, even though I am live,
it's not this, ah, okay, this, this is the code
using only repeats, left, pen up and pen downs
that will create this particular pattern
and if I go and grab this, I've pasted it here
and if I go back to my example and I paste it in...
(drum roll)
(buzzer buzzing) It doesn't work.
What's wrong?
Well, weirdly, I'm going to show you something weird.
If I put a space here and a space here, ah,
all of a sudden.
(horns playing) We have the pattern
we were looking for all long.
But there's some weird buggy stuff going on,
why is it a space but no space, here is the issue.
So it took a while to debug, I will show you a way
of finding, the way that I found the error was by looking
through, first of all the chat found this way before me
and kept suggesting it and I had to look and look and look.
Where I found it all of a sudden in this last repeat here,
this command right, right, it didn't pick up
the argument three, it's blank so that's certainly why
I'm not getting the pattern that I'm expecting to get.
Now why is it doing that?
Well the secret to that, it's not a secret so much,
lies in the fact of the way that I parsed this bracket,
right, remember if I have a repeat,
if I go here, right, I look for the first opening bracket,
that's the start, the first ending bracket
and that's the end but what if there's a nested repeat?
If there's a nested repeat, this is the starting bracket,
this is the ending bracket.
So it completely ignores, it pulls it off here
and then loses the way it's parsing, it loses that three
and somehow having a space in between there,
it kind of is able to still get that three by accident.
So what I really need to do is match the number of brackets.
All right, I have a different idea now.
What if, I think this finding the first one
and finding the last one in this weird double wild loop way
is kind of silly because all I need to do is keep track of,
I mean I do need to find the first one,
so let's stick with finding the first one
and then basically once I found that first one,
bracket, bracket count,
ah,
equals one.
So now, what I want to do is while bracket count
is greater than zero, I'm going to look at the character.
If character = and opening bracket,
then, ah, then increase the bracket count,
else if it = a closing bracket,
then decrease the bracket count.
And then I should have,
end should just now be wherever
the index is, wherever it last left off,
I might need to do minus 1, I'm not sure, right,
so this should be like, I'm basically finding
the first bracket, in this case,
there's noise and spaces there
and then I'm going to pull out,
I'm going to, I'm basically looking for where
that last bracket is and the last bracket is
and as the last bracket comes, bracket count will be zero,
I'll be done but if there are other brackets,
it's got to keep counting, okay.
(drum roll)
I should never use my drum sound because it always means
it's not going to work.
(clapping) (train whistle)
All right, there we go.
All right so this is complete, it works,
I'm very excited about that.
People have pointed out some examples
that I found, for example, I could try to make
this design, let's see what happens
if I post this in here.
Right, we could say, oh I'm kind of off
where my starting point is of but now,
I could start to really be creative and the line
is sort of thick here so I also feel like now that I'm doing
more complex stuff, I probably want
to change the stroke weight back to one.
And we can see that looks a little bit nicer.
I just want to show you something though amazing.
If we go back to the Logo repo which this is the state
before I've done this video and I go to pull requests
and I go, for example, to pull request number three
from TheTastefulToastie, you can see here are a whole bunch
of other ones that you can see that I could now try
that should work with my, and I'm going to show you something
that's going to blow your mind, this.
Ah, let's make this work, we have to make this work.
So I'm going to go, just before I leave,
think about other creative things you could add.
These are all the commands.
I am going to now go into index and HTML,
I'm going to put that in here, hold on, what's going on here?
Let me give a carriage return here,
let me paste in all of those commands.
Now I could now go back to here and do this.
We don't see anything.
So one thing that I really definitely need to do
is give it a much bigger canvas.
So let's go back to create canvas.
Have to finish with this.
900 by 900,
something like that, refresh.
(gasps)
There it is.
Oh my goodness, this is nuts.
But it's a little off center, oh let's change it to 20 20
or zero, let's actually just change it to 0, 0.
And
make it a little bit wider.
Whoops.
There we go.
(train whistle)
it is The Coding Train logo made with literally with Logo
and all of these numbers here.
I hope you enjoyed this video,
there's so many things missing, right,
there are all these Logo, if you look at the history
of Logo and you look at the language of Logo,
there are lots of commands that I missed.
So I'll list some of those in the video's description,
try to implement those or also, color,
how would you incorporate color with this?
How would you think of other clever ways
to make this interactive in realtime
to allow people to play with it?
So, we now have a full interpreter here
and I'm finished with this project.
I can't wait to see what you make,
I'm going to add this code to the This Coding Train Logo
repository and I will accept pull requests
for implementing other commands, bug fixes,
adding things like color, making the interface
a little nicer so I'm probably going to close out most
of those pull requests that were implementations
of repeat, I thank you for them but this will be now
a community Logo interpreter project that will start
from the code that is in this video.
Goodbye, everyone, and see you in future coding challenges.
(upbeat music)