-
Notifications
You must be signed in to change notification settings - Fork 15
7ML7W Factor Day 3 Balancing on a Boat
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.
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 Struct
s 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 tof
(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.)
- the
T{ }
syntax for initializing tuples with specific, named fields populated (much like Python's keyword arguments)
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.
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.
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:
- We need to
dup
thenumber
asmod
will consume it and we need to keep a copy around in case we need to return it; - Because we have the extra copy of
number
, we need to explicitlydrop
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...
🎉
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!
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.
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.
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
Huge thanks to @charlieegan3 for organising the meeting (particularly with the late venue change) and to all those who brought bread, dips and snacks.
- Home
- Documentation
- Choosing a Topic
- Shows & Tells
- Miscellaneous
- Opt Art
- Reinforcement Learning: An Introduction
- 10 Technical Papers Every Programmer Should Read (At Least Twice)
- 7 More Languages in 7 Weeks
- Lua, Day 1: The Call to Adventure
- Lua, Day 2: Tables All the Way Down
- Lua, Day 3
- Factor, Day 1: Stack On, Stack Off
- Factor, Day 2: Painting the Fence
- Factor, Day 3: Balancing on a Boat
- Elm, Day 1: Handling the Basics
- Elm, Day 2: The Elm Architecture
- Elm, Day 3: The Elm Architecture
- Elixir, Day 1: Laying a Great Foundation
- Elixir, Day 2: Controlling Mutations
- Elixir, Day 3: Spawning and Respawning
- Julia, Day 1: Resistance Is Futile
- Julia, Day 2: Getting Assimilated
- Julia, Day 3: Become One With Julia
- Minikanren, Days 1-3
- Minikanren, Einstein's Puzzle
- Idris Days 1-2
- Types and Programming Languages
- Chapter 1: Introduction
- Chapter 2: Mathematical Preliminaries
- Chapter 3: Untyped Arithmetic Expressions
- Chapter 4: An ML Implementation of Arithmetic Expressions
- Chapter 5: The Untyped Lambda-Calculus
- Chapters 6 & 7: De Bruijn Indices and an ML Implementation of the Lambda-Calculus
- Chapter 8: Typed Arithmetic Expressions
- Chapter 9: The Simply-Typed Lambda Calculus
- Chapter 10: An ML Implementation of Simple Types
- Chapter 11: Simple Extensions
- Chapter 11 Redux: Simple Extensions
- Chapter 13: References
- Chapter 14: Exceptions
- Chapter 15: Subtyping – Part 1
- Chapter 15: Subtyping – Part 2
- Chapter 16: The Metatheory of Subtyping
- Chapter 16: Implementation
- Chapter 18: Case Study: Imperative Objects
- Chapter 19: Case Study: Featherweight Java
- The New Turing Omnibus
- Errata
- Chapter 11: Search Trees
- Chapter 8: Random Numbers
- Chapter 35: Sequential Sorting
- Chapter 58: Predicate Calculus
- Chapter 27: Perceptrons
- Chapter 9: Mathematical Research
- Chapter 16: Genetic Algorithms
- Chapter 37: Public Key Cryptography
- Chapter 6: Game Trees
- Chapter 5: Gödel's Theorem
- Chapter 34: Satisfiability (also featuring: Sentient)
- Chapter 44: Cellular Automata
- Chapter 47: Storing Images
- Chapter 12: Error-Correcting Codes
- Chapter 32: The Fast Fourier Transform
- Chapter 36: Neural Networks That Learn
- Chapter 41: NP-Completeness
- Chapter 55: Iteration and Recursion
- Chapter 19: Computer Vision
- Chapter 61: Searching Strings
- Chapter 66: Church's Thesis
- Chapter 52: Text Compression
- Chapter 22: Minimum spanning tree
- Chapter 64: Logic Programming
- Chapter 60: Computer Viruses
- Show & Tell
- Elements of Computing Systems
- Archived pages