Once more there’s been a hiatus in my Seven more languages in seven weeks odyssey. This time caused by a large amount of social commitments. I had actually done most of the exercises a couple of weeks ago and not typed them up.
Factor is a nice little language and concatenative programming is a good thing in general. But I have struggled in getting google or ddg to help me with the language. I suppose factor is such a common word that you have to already know what you seek before you start to search. My search strategy tends to be more speculative on occasion.
Incidentally, on the subject of the value of the concatenative approach, I came across a great piece called Why Concatenative Programming Matters (after the famous John Hughes paper Why Functional Programming Matters [PDF]), by Jon Purdy — it was almost enough to convince me that the pain of googling for common words might be outweighed by the merit of what I was learning. Almost ;-)
Exercises
Easy exercises
Create an examples.strings vocabulary and write a word named palindrome? that takes a string from the stack and returns t or f according to whether or not the word is a palindrome.
Fairly trivial this one a palindrome is equal to the reverse of a copy of itself. USING: kernel sequences ;
IN: day2.examples.strings
: palindrome? ( str -- ispal ) dup reverse = ;
In the appropriate vocabulary for associating tests with the example.strings vocabulary, write 2 unit tests for pplaindrome?, 1 that expects t and one that expects f.
USING: day2.examples.strings tools.test ;
IN: day2.examples.strings.tests
{ t } [ "racecar" palindrome? ] unit-test
{ f } [ "racecat" palindrome? ] unit-test
Add the examples.strings to the test suite so that its tests are included when running test-suite.factor.
This is just a case of adding a single line. Very convenient. USE: day2.examples.strings
Medium exercises
Create an examples.sequences vocabulary and write a find-first word that takes a sequence of elements and a predicate and returns the first element for which the predicate returns true. Write a correcponding unit test that confirms its behaviour. What happens if none of the elements satisfy the predicate.
I started with the unit tests (honest). It looks like the normal way to deal with missing values in Factor is to return f, so that will be my expected behaviour.
USING: kernel io math math.parser tools.test ;
USE: day2.examples.sequences
IN: day2.examples.sequences.tests
{ 2 } [ { 1 2 3 4 } [ 2 >= ] find-first ] unit-test
{ 3 } [ { 1 2 3 4 } [ 2 > ] find-first ] unit-test
{ f } [ { 1 2 3 4 } [ 9 > ] find-first ] unit-test
I ended up writing 2 versions of the code. The first uses some built in libraries, looks cleaner and is easier to understand (IMO) USING: sequences kernel io ;
IN: day2.examples.sequences
: find-first ( seq pred -- elt ) partition drop ?first ; inline
The compiler was very insistent on my adding that inline after the function declaration. Who am I to argue?
I did a second implementation which was perhaps more in keeping with the spirit of the exercise. But it is longer, uglier and harder to understand. ! Here's a version using only the shuffling words and a loop
: find-first' ( pred seq -- elt )
[ 2dup dup length 0 =
[ 4drop f f ]
[ first swap call
[ swap drop first f ]
[ rest t ] if
] if
] loop ; inline
You’ll notice that the arguments are in a different order on the stack. That was just for my own convenience. Props to Factor for allowing the ' character in identifiers. That is something I have grown to like a lot in Haskell and miss in other languages now.
Of course find-first' needs some unit tests too. Here they are. { 2 } [ [ 2 >= ] { 1 2 3 4 } find-first' ] unit-test
{ 3 } [ [ 2 > ] { 1 2 3 4 } find-first' ] unit-test
{ f } [ [ 9 > ] { 1 2 3 4 } find-first' ] unit-test
In an examples.numberguess vocabulary, write a standalone program that picks a random number from 1 to 100 and asks the user to guess, printing "Higher", "Lower", or "Winner" accordingly.
A good challenge this one and one that melted my brain for a bit. Here is my attempt. USING: random sequences kernel math math.parser io ;
IN: day2.examples.numberguess
: win? ( num guess -- whl num guess )
2dup =
[ "Winner" ]
[
2dup <
[ "Lower" ]
[ "Higher" ] if
] if ;
: ask-num ( -- ) "Guess a number? " write flush ;
: read-num ( -- n ) readln string>number ;
: try ( n -- n g ) ask-num read-num win? print ;
: game ( n -- n ) [ dup try = not ] loop ;
: choose-num ( -- n ) 100 random 1 + ;
: play ( -- ) choose-num game drop ;
MAIN: play
Hard exercises
Enhance the test-suite.factor program so that it prints out how many tests have run, and in case of failures, how many tests have failed.
This took ages, mostly because of the google-fu fail I had happening. I just couldn’t find what I was after on ddg or google. Got there in the end. Programming is sometimes like banging your head against a brick wall — it feels great when you stop! USING: tools.test io io.streams.string strings
splitting kernel namespaces sequences math.parser math regexp ;
USE: day2.examples.hello
USE: day2.examples.strings
USE: day2.examples.sequences
USE: day2.examples.greeter
IN: day2.examples.test-suite
! rather than using a null writer use a string writer,
! split output into lines, filter lines that start
! Unit Test count them and pop that on the stack
: test-all-examples ( -- )
[ "day2.examples" test ] with-string-writer
>string string-lines
[ R/ ^Unit Test:.*/ matches? ] filter length
test-failures dup get empty?
[ drop "w00t! All " write number>string
" tests passed!" append print ]
[ :test-failures
get length number>string "/" append write
number>string " tests failed :-(" append
print
] if ;
MAIN: test-all-examples
As you can probably imagine this roundabout way of solving the problem seems unsatisfying to me. It feels too kludgey.
Make test-suite.factor interactive by turning it into a command-line program that asks the user which vocabularies to test via the console, then runs the tests and outputs the results.
OK, I admit I did not complete this one. Will probably have a crack at some point. When I have more time to spend on it.