Monday, September 27, 2010

Fortune-telling

One program that some new users of Unix-like systems are introduced to when learning the command-line is fortune. The purpose of the program is simple: to display a random message from a database of quotations.

Note: fortune is available on the Mac OS X using MacPorts (port install fortune), Ubuntu (apt-get install fortune-mod), and Fedora (yum install fortune-mod).

A demonstration of running it:

$ fortune
The human race has one really effective weapon, and that is laughter.
  -- Mark Twain

I thought it would be fun to get this to work in Factor. We're going to do it two different ways. The first way is to use the io.launcher vocabulary for launching processes and printing the output directly to the listener.

( scratchpad ) "/opt/local/bin/fortune" utf8 
               [ contents print ] with-process-reader
It'll be just like Beggars Canyon back home.
  -- Luke Skywalker

Another way is to parse the fortune files directly, and then choose a random quotation to print (i.e., implementing the fortune logic inside of Factor). First, we will need to list the vocabularies we will be using and define a "fortune" namespace:

USING: io io.encodings.ascii io.files kernel make memoize
random sequences splitting strings ;

IN: fortune

The fortune program uses data files which are stored in a "cookie-jar format". From faqs.org:

It is appropriate for records that are just bags of unstructured text. It simply uses newline followed by %% (or sometimes newline followed by %) as a record separator.

Parsing a fortune data file (given a string containing the contents in cookie-jar format) is pretty straightforward. We are using slices which allow us to keep pointers to sections of text within the data file.

: parse-fortune ( str -- seq )
    [
        [ "%\n" split1-slice dup ]
        [ swap , ] while drop ,
    ] { } make ;

: load-fortunes ( path -- seq )
    ascii file-contents parse-fortune ;

The fortune data files are installed in different locations on different systems, so we can define a search path which includes some of the locations that are often used. We can then use this list to load the fortunes from the first data file that exists (and memoize the results for performance):

CONSTANT: FORTUNES {
    "/usr/games/fortune/fortunes"
    "/usr/share/games/fortune/fortunes"
    "/usr/local/share/games/fortune/fortunes"
    "/opt/local/share/games/fortune/fortunes"
}

MEMO: default-fortunes ( -- seq )
    FORTUNES [ exists? ] find nip load-fortunes ;

Getting a fortune is then just a matter of choosing a random quotation and printing it out:

: fortune ( -- )
    default-fortunes random >string print ;

If you want, you could define a MAIN: entry point and use the deploy tool to make this into a deployed binary.

The source code for this is available on my Github.

Update: Doug Coleman suggested a one-liner that can produce the same result:

"/opt/local/share/games/fortune/fortunes" ascii file-lines
{ "%" } split random [ print ] each

No comments: