Charlie Harvey

Seven More Languages: Factor Day Two

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 ;-)


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.


  • Be respectful. You may want to read the comment guidelines before posting.
  • You can use Markdown syntax to format your comments. You can only use level 5 and 6 headings.
  • You can add class="your language" to code blocks to help highlight.js highlight them correctly.

Privacy note: This form will forward your IP address, user agent and referrer to the Akismet, StopForumSpam and Botscout spam filtering services. I don’t log these details. Those services will. I do log everything you type into the form. Full privacy statement.