Skip to content

7ML7W Factor Day 3 Balancing on a Boat

Paul Mucur edited this page Mar 21, 2018 · 17 revisions

After some eleventh hour logistical changes, we all gathered in the Altmetric office with poorly poured drinks, some topic-appropriate Stax® and more than one loaf of bread per attendee.

We decided to follow @h-lame's suggested meeting plan and kicked things off by looking at Factor's object system.

Tuples

TUPLE: cart-item name price quantity ;

Upon our first encounter with tuples, we had a brief discussion of the concept of "tuples" in other languages such as Python where it typically describes a positional data structure: that is, a data structure without named fields but with elements identified only by their position. Factor's "tuples" seemed much closer to records like those we saw in TAPL or Structs in Ruby. We heard rumour that this may be similar to Common Lisp's Object System but no-one knew for sure whether this was true or not.

Those of us who had attempted to follow along with the code samples in the chapter remarked that the code does not work out of the box and requires some extra shenanigans with vocabularies. In particular, we discussed how the curious accessor syntax for fields (e.g. >>foo for writing and foo>> for reading) are only available if we USE the accessors vocabulary. What's more, this hints at vocabularies being able to generate words dynamically: previously, we'd thought of vocabularies as static collections of words much like a module or a library but accessors will automatically generate words based on our declared tuple types.

We breezed through the three ways of constructing a tuple:

  • new: initializing an empty tuple with all its fields set to f (Factor's false)
IN: scratchpad cart-item new

--- Data stack:
T{ cart-item f f f f }

(As as aside, we noticed that tuples always have an extra first field set to f.)

  • the hilariously-named boa constructor (get it?)
IN: scratchpad "Alice" 5.99 300 cart-item boa

--- Data stack:
T{ cart-item f "Alice" 5.99 300 }

(We discussed the order of arguments here: boa stands for "by order of arguments" so it was curious that cart-item is the last on the stack. We convinced ourselves this made sense because the last argument is really the top of the stack but then that means the other arguments on the stack are in reverse order. We tried not to think about this too much.)

IN: scratchpad T{ cart-item { price 5.99 } }

--- Data stack:
T{ cart-item f f 5.99 f }

Happy that all of this made sense, we decided to skip the rather involved checkout example in the chapter and get right to the fun bit: implementing FizzBuzz in Factor.

FizzBuzz

We decided to uphold the club's longheld tradition of test-driving everything so we engaged in the required .factor-roots hijinks so we could attempt our first implementation of FizzBuzz.

A naïve implementation

We decided to design our solution as a word fizzbuzz which, given a number, returns either "Fizz", "Buzz", "FizzBuzz" or the original number as specified in the original problem.

We began with the simplest test case:

USING: tools.test example.fizzbuzz ;
IN: example.fizzbuzz.tests

{ 1 } [ 1 fizzbuzz ] unit-test

And wrote just enough code to make the test pass:

USING: kernel ;
IN: example.fizzbuzz

: fizzbuzz ( number -- result ) 1 ;

But lo! This did not work because Factor (correctly) picked us up on the fact that the stack effect of fizzbuzz is meant to consume the number on the stack and it does not. Nae bother, we quickly fixed this:

USING: kernel ;
IN: example.fizzbuzz
: fizzbuzz ( number -- result ) drop 1 ;

Success!

Onto the next test case:

{ "Fizz" } [ 3 fizzbuzz ] unit-test

To make this work, we introduced our first conditional--a when--which would switch based on whether number is divisible by 3 or not by using mod and comparing the result with 0:

USE: math

: fizzbuzz ( number -- result ) dup 3 mod 0 = [ drop "Fizz" ] when ;

Note we have do some stack manipulation here:

  1. We need to dup the number as mod will consume it and we need to keep a copy around in case we need to return it;
  2. Because we have the extra copy of number, we need to explicitly drop it if we're returning "Fizz"

Next up: Buzz!

{ "Buzz" } [ 5 fizzbuzz ] unit-test

Now we can expand our when into an if using the same technique:

: fizzbuzz ( number -- result )
  dup 3 mod 0 =
  [ drop "Fizz" ]
  [
    dup 5 mod 0 =
    [ drop "Buzz" ]
    when
  ]
  if ;

Finally, FizzBuzz!

{ "FizzBuzz" } [ 15 fizzbuzz ] unit-test

Heck, let's nest some more conditionals!

: fizzbuzz ( number -- result )
  dup 15 mod 0 =
  [ drop "FizzBuzz" ]
  [
    dup 3 mod 0 =
    [ drop "Fizz" ]
    [
      dup 5 mod 0 =
      [ drop "Buzz" ]
      when
    ]
    if
  ]
  if ;

Success! Let's see if we can complete the full problem with this implementation:

IN: scratchpad 100 [1,b] [ fizzbuzz ] map

--- Data stack:
{ 1 2 "Fizz" 4 "Buzz" "Fizz" 7 8 "Fizz" "Buzz" 11 "Fizz" 13 14...

🎉

ChrizzTuzz

That was all well and good but didn't seem very idiomatic or, shall we say, concatenative. We wondered if there might be a way to refactor our code when @tuzz suggested that we break our solution up into words that consume a str and a number on the stack and leave a mutated str: this would let us incrementally append either "Fizz", "Buzz" or the number itself (meaning we could skip the conditional for 15 altogether). We hereafter referred to this angle of attack as "ChrizzTuzz".

We first modified our tests to check for string values (thereby quelling previous complaints from our resident type pedants that our previous implementation could return either a number or a string):

{ "1" } [ 1 fizzbuzz ] unit-test
{ "2" } [ 2 fizzbuzz ] unit-test
{ "Fizz" } [ 3 fizzbuzz ] unit-test
{ "Buzz" } [ 5 fizzbuzz ] unit-test
{ "FizzBuzz" } [ 15 fizzbuzz ] unit-test

Then we began by declaring a new word for fizz:

USE: sequences

: fizz ( str number -- str ) 3 mod 0 = [ "Fizz" append ] when ;

This takes a str and a number, checks if the number is divisible by 3 (thereby consuming number) and appends the string "Fizz" if so. If not, it does nothing, leaving only the original str.

Next, buzz:

: buzz ( str number -- str ) 5 mod 0 = [ "Buzz" append ] when ;

Similar to fizz, except checking if number is divisible by 5 and appending "Buzz" if so.

Then a tricky one, our word for numbers that are neither divisible by 3 nor 5:

USE: math.parser

: neither ( str number -- str )
    swap dup empty? [ drop number>string ] [ nip ] if ;

This does some stack shenanigans to first put the str on top of the stack, take a copy of it with dup and then check if it is empty? (consuming the copied string); if so, it drops the original str from the top of the stack and converts number into a string. If it's not empty?, nip the number from the stack, leaving only the str.

Now we can glue them all together in fizzbuzz:

: fizzbuzz ( number -- result )
    "" over fizz over buzz over neither nip ;

This starts us off by pushing an empty string onto the stack in front of the given number then copies the number with over giving us a stack like so:

number
""
number

Given that we now have a string str and a number number, we can call our fizz word which will consume both and leave the following on the stack:

number
str

We then copy number with over again and call buzz; lather, rinse and repeat with neither leaving us with the number and final str. We simply nip the now-redundant number off the stack and we have our result!

Does it work?

IN: scratchpad 100 [1,b] [ fizzbuzz ] map

--- Data stack:
{ "1" "2" "Fizz" "4" "Buzz" "Fizz" "7" "8" "Fizz" "Buzz" "11"...

Huzzah!

The book's implementation

Satisfied that we'd come up a slightly more idiomatic solution together, we decided to review the book's implementation:

: mult? ( x/str n -- ? ) over number? [ mod 0 = ] [ 2drop f ] if ;

: when-mult ( x/str n str -- x/str ) pick [ mult? ] 2dip ? ;

: fizz     ( x/str -- x/str )  3 "Fizz"     when-mult ;
: buzz     ( x/str -- x/str )  5 "Buzz"     when-mult ;
: fizzbuzz ( x/str -- x/str ) 15 "FizzBuzz" when-mult ;

: fizzbuzz-pipeline ( x -- str ) fizzbuzz fizz buzz present ;

We were pleasantly surprised to also see separate words for fizz and buzz but had to stare at the definitions of mult? and when-mult for a long time to understand what was happening.

While mult? seemed straightforward enough (returning whether or not the penultimate thing on the stack was a multiple of the last thing on the stack), when-mult baffled us at first.

: when-mult ( x/str n str -- x/str ) pick [ mult? ] 2dip ? ;

We decided to walk through it one step at a time, starting with the following stack:

1
3
"Fizz"

The call to pick acts like over but copies the third (rather than the second) thing on the stack, giving us:

1
3
"Fizz"
1

The confusing call to [ mult? ] 2dip then effectively stashes the top two things on the stack:

1
3

It then calls mult? to see if 1 is a multiple of 3 (it is not):

f

It then restores the two previously-stashed values:

f
"Fizz"
1

Then it interprets the whole stack as a ternary expression with ? (think f ? "Fizz" : 1), giving us what we want:

1

If the number had been divisible by 3, we would have instead returned "Fizz".

Understanding but still a little wary we skipped the exercises and decided to move on to cover the chapter's discussion of Factor's strengths and weaknesses.

Factor, then

It's fair to say that the reaction to Factor in the club has been... mixed.

Channelling Dan McKinley a little, we discussed whether Factor had spent a few too many "innovation tokens" by having so many unfamiliar things in the language. Some of us found the unfamiliar nature of Factor precisely why it was so enjoyable to play with and we wondered whether Forth would have been a better introduction to concatenative programming.

Retrospective

With the Factor part of the book done, we reflected on how the book is going so far.

  • We agreed that this meeting went well thanks to @h-lame's suggested plan of skipping large sections of the chapter and all the exercises but focussing mostly on FizzBuzz
  • We were initially dismissive of the checkout example in this chapter but a good point was made that it may have been a gentler introduction into a true, concatenative style of programming which we missed by jumping straight into FizzBuzz
  • We posited that we may have been spoilt by how thoroughly thought-out our previous club texts have been (with so many of them being textbooks and course material) and that we may need to go off-book more in future chapters to get the most out of the topics
  • @tomstuart kindly agreed to organise the next meeting including picking a new day for us to meet for the next language: Elm

Thanks

Huge thanks to @charlieegan3 for organising the meeting (particularly with the late venue change) and to all those who brought bread, dips and snacks.

Clone this wiki locally