Tracks
/
Factor
Factor
/
Exercises
/
Telegrapher's Tape
Telegrapher's Tape

Telegrapher's Tape

Learning Exercise

Introduction

You've already used streams in channel-chatter and disposed of them in boatswains-bilge. This exercise is your first custom stream — a class that implements the protocol so the same with-input-stream / read1 / stream-read1 code that worked on a <string-reader> works on yours.

Mixins: the other half of the class hierarchy

You already know TUPLE: child < parent … from dragons-descendants — a single-parent hierarchy where slots flow down. Factor has a second way to extend the class graph: open mixins. A MIXIN: m declares a class with no slots that other classes can join after the fact with INSTANCE: c m. Membership is many-to-many: a class can be an instance of as many mixins as it likes.

The streams protocol uses two mixins to mark which half of the interface a class implements:

INSTANCE: my-stream input-stream    ! readable
INSTANCE: my-stream output-stream   ! writable

A stream can be one, the other, or both. Generic methods on input-stream (or output-stream) then dispatch to any class that has joined the mixin — that's how the ambient read1, stream-contents, with-input-stream, etc. find their way to your class.

Mixins are open: you can add a new member after the mixin is declared, even in a different vocabulary. That's the contrast with TUPLE: < parent, where the relationship is fixed when the child class is declared.

The protocol generics

The protocol generics live in io:

stream-read1         ( stream -- elt/f )
stream-read          ( n stream -- seq/f )
stream-read-unsafe   ( n buf stream -- count )
stream-write1        ( elt stream -- )
stream-write         ( seq stream -- )
stream-flush         ( stream -- )
stream-element-type  ( stream -- type )

stream-read1 returns the next byte (or f at end-of-stream); stream-write1 writes one. stream-read-unsafe is the lower-level batch read most input streams provide; stream-read is built on top of it and handles allocation. stream-element-type says whether the stream deals in bytes (+byte+) or characters (+character+); when you wrap another stream, the simplest implementation just delegates to the underlying one.

Cleanup is from destructors

Streams are disposables: a custom stream extends disposable and implements dispose*. That's the framework from boatswains-bilge, applied to a stream class:

TUPLE: my-stream < disposable underlying ;

: <my-stream> ( underlying -- s )
    my-stream new-disposable swap >>underlying ;

M: my-stream dispose* underlying>> dispose ;

Once that's in place, all the with-… combinators from channel-chatter work on your stream automatically.

Instructions

You're the wireless telegrapher on a freighter. The signal comes in over a noisy line — ship's machinery, weather static, the occasional foreign chatter — interleaved with the actual Morse you're listening for. To catch the message you feed the wire through a tape device that strips out everything except valid Morse symbols (., -, and the space that separates letters).

Your job is to build the tape as a custom Factor input stream.

1. Define the tape

Define a tape tuple that extends disposable and holds one slot, wire, for the underlying input stream.

TUPLE: tape < disposable wire ;

Then write the constructor <tape> ( wire -- tape ). Use new-disposable (not new) so the destructor framework registers the tape, and store the underlying stream in the wire slot.

2. Join the input-stream mixin

Declare tape to be an input-stream:

INSTANCE: tape input-stream

That makes the protocol's default combinators dispatch to your class.

3. Read one Morse symbol

Implement M: tape stream-read1 ( tape -- elt/f ). Read bytes from the underlying wire until you hit one of CHAR: ., CHAR: -, or CHAR: \s (space). Return that byte. If the wire returns f (end-of-stream), return f too — the tape is done.

"x.y -ab cz" <string-reader> <tape>
[ stream-read1 ] [ stream-read1 ] [ stream-read1 ] tri
! => CHAR: . CHAR: - CHAR: \s

4. Delegate the element type

Implement M: tape stream-element-type ( tape -- type ) by delegating to the wire — the tape doesn't change the kind of elements being read.

5. Clean up

Implement M: tape dispose* ( tape -- ). The framework calls your method exactly once per dispose; just dispose the underlying wire.

With those five pieces in place the tape works as a regular input stream — with-input-stream, stream-read, and the other protocol words all dispatch to your methods automatically.

Edit via GitHub The link opens in a new window or tab
Factor Exercism

Ready to start Telegrapher's Tape?

Sign up to Exercism to learn and master Factor with 47 concepts163 exercises, and real human mentoring, all for free.