Code Log: In which we split a string with RetroForth
Today’s goal? To write a bit of retro that mimic’s JavaScript’s inbuilt .split()
function.
Retro ships with both s:split/string
and s:split/char
. They work the same way as far as I can tell, but one uses a string pattern to split on while the other splits based on a single character.
In JavaScript if I write something like,
let str = "banana"
str.split("a")
// Returns an array:
// => [ "b", "n", "n", "" ]
Meanwhile, with retro’s s:split/string
I get something different back.
'banana 'a s:split/string s:put nl s:put nl
That returns 2 strings,
b
anana
That isn’t what I want!
So, back to our challenge! How to get retro to mimic JavaScript’s .split()
?
As a first step, I’m going to explore s:replace-all
.
From retro’s glossary:
s:replace-all
Data: sss-s Addr: - Float: -
Replace all instances of s2 in s1 with s3.
So, the following should return the string bXnXnX
'banana 'a 'X s:replace-all s:put
This indicates to me that retro already has a way to work over each element of a string, and I can rely on some inbuilt magic, perhaps, to inspect each element of a string.
The next words that seem worth exploring are s:map
and s:filter
. Both of these words apply a quote to each element of a string.
Again, from the glossary:
s:map
Data: sq-s Addr: - Float: -
Execute the specified quote once for each character in the string. Builds a new string from the return value of the quote. The quote should return only one value.
And then,
s:filter
Data: sq-s Addr: - Float: -
Execute the quote once for each value in the string. If the quote returns
TRUE
, append the value into a new string. IfFALSE
the value will be discarded.
I think that s:filter
is where I want to start.
'banana [ 'a s:eq? ] s:filter s:put nl
Reading the description of how s:filter
works I guessed that the above example would return something like 'aaa
Instead it returns nothing…so, that isn’t heaps useful to me at the moment.
'banana [ nl 'boom! s:put ] s:map
Meanwhile, the above returns boom!
once for each letter in banana!
BANG! That is something!
…but then I had a sudden realization — I’m a goober caught off guard by a type system — classic!
Rewinding to s:filter
!
'banana [ $a eq? ] s:filter s:put nl
The issue was that I was checking for string equality when s:filter
is breaking the string into individual…you guessed it (you probably figured it out before I did)…characters!
So, the above now does indeed return 'aaa
!
COOKING WITH FIRE! Next steps!? How do we leverage our new-found Promethean-power to split strings?
I think s:map
may still be what we want to use, but now we know to do it with characters instead of strings.
A first attempt:
'banana [ $a eq? [ nl 'BOOM_it's_a_match! s:put nl ] if ] s:map
My first thought was to nest two quotes to check for a matching character. This doesn’t work, though. I’m not 100% certain why. The documentation says that nested quotes are kosher and my inner quote works a-okay when it is tested on its own:
$a $a eq? [ nl 'BOOM_it's_a_match! s:put nl ] if
Perhaps s:map
isn’t passing characters into the quotation like with s:filter
? We can test that theory easily enough:
'banana [ $a eq? [ nl 'Match s:put nl ] if ] s:filter s:put nl
That also doesn’t work — interestingly, though, it doesn’t work in the exact same way that s:map
didn’t work! In both instances I was dropped into the repl when I ran retro split.retro
(the name of this file). The expected result was for the code to be run, stuff be barfed to stdout
and then to be returned to the shell. I’m not sure how to act on this new information, but I’m gonna stash it away as something of note for the time being. Another clue. Another Scooby Snack.
A return to s:filter
, and abandoning nested quotes for the time being.
Success is in sight!
Rather than use a nested quotation, lets filter out all the $a
in 'banana
, then, because there is no way to visualize an array in retro, lets iterate over that array to show each element in it! This is looking a whole lot like .split()
in JavaScript!
'banana [ $a -eq? ] s:filter a:from-string [ c:put nl ] a:for-each
GLORIOUS! Success — the above code returns the following,
b
n
n
It was a bit of a journey but I’ve written a bit of retro that can mimic the behavior of .split()
, at least at first blush.
Thanks to the heroic efforts of a friend on IRC it was pointed out that the issue with my nested quotes is nothing to do with their being nested quotes and everything to do with the stack being unbalanced!
'banana [ $a eq? dup [ nl 'Match s:put nl ] if ] s:filter s:put nl
Gotta dup
!
Discussion of this post on /r/Forth.
Published