Squeak
  links to this page:    
View this PageEdit this PageUploads to this PageHistory of this PageTop of the SwikiRecent ChangesSearch the SwikiHelp Guide
Fun with streams
Last updated at 8:59 am UTC on 19 September 2016
Update 2016:

The selector #asFilename needs to be replaced with with #asFileName.
However the semantics of
 'myfile' asFileName readStream
are different in Squeak and thus the exercises below using this do not work in Squeak as is.



13 June 2004
Suppose you wanted to count the number of spaces in a string. There are many ways to do it in Smalltalk. One of the shortest ways is:
aString occurrencesOf: Character space.

A longer way, but a little faster, and more like the way you'd do it in another language, is:
	| count |
	count := 0.
	aString do: [:each | each = Character space ifTrue: [count := count + 1]].
	count

The second way works with streams, too. The only benefit to using streams, instead of strings, is that files are usually treated as streams. If the data started out in a string we'd use:

	| count |
	count := 0.
	(ReadStream on: aString) 
		do: [:each | each = Character space ifTrue: [count := count + 1]].
	count

or if it were in a file we'd use:

	| count |
	count := 0.
	'myfile' asFileName readStream 
		do: [:each | each = Character space ifTrue: [count := count + 1]].
	count

Perhaps the most complex way is:

	| count |
	count := 0.
	file :=  'myfile' asFileName readStream .
	[file atEnd] whileFalse: [file next = Character space ifTrue: [count := count + 1]].
	count


But, suppose we wanted to count the number of spaces before the first 'X'. The most complex way becomes the simplest.

	| count  nextChar|
	count := 0.
	file :=  'myfile' asFileName readStream .
	[file atEnd or: [nextChar := file next.  nextChar = $X]] 
		whileFalse: [nextChar = Character space ifTrue: [count := count + 1]].
	count

Well, it isn't really the simplest. The hard part is breaking out of the loop when you find an 'X'. A good way to break out of a loop is to put the loop in a method and to break out of it by returning from the method.

	countSpaces: aStream
		| count |
		count := 0.
		aStream do: [:each | 
				each = 'X' ifTrue: [^count].
				each = Character space ifTrue: [count := count + 1]].
		^count


Ideas for exercises


1) Count the number of lines in a file. "Character cr" ends a line, at least in Unix.

2) Count the number of lines in a file with the letter 'Z' in them.

3) Print on the Transcript each line that has the letter 'Z' in it. To print a string on the transcript, use "Transcript show: aString". The best way to save a line in case you want to print it out is to put it in a WriteStream. You can create a WriteStream on a string by saying 'WriteStream on: String new', you can put a character on it with nextPut:, and you can get a stream's contents with the 'contents' message.

4) Assume there is a file called 'phonebook' that consists of lines with two fields. The first field is a name, and it is terminated by a tab character. The second field is a phone number, and it is terminated by an end of line character (i.e. "Character cr" on Unix). Given a name, print on the transcript the phone number that goes with it.

5) Make a class called "Phonebook". When you create an instance of Phonebook, you must initialize it with the name of the file that contains the phonebook data. You might do this by sending the "dataFrom:" message to it. Implement a method "numberFor:" that takes a name and searches the file, returning the corresponding phone number. The phone number is actually a string, by the way, not a number.

6) Searching a file can be slow. If the phone book is not too large, you can keep it all in memory. Make a new version of Phonebook that keeps a dictionary that maps names to phone numbers, but that has the same interface as before. Now, when you create an instance of Phonebook and initialize it with the name of the file, it should read the entire file and initialize the dictionary. The "numberFor:" method should now use the dictionary to get the phone number, not the file.

7) Make a publishOn: method for Phonebook that takes a stream as an argument and prints the names and numbers in the Phonebook, sorted by name. The usual way to sort something in Smalltalk is to use a SortedCollection. You must parameterize it with a block. See "Sorting in Smalltalk".

8) Make a phonesOn: method for Phonebook that takes a stream as an argument and prints the names and numbers in the Phonebook, sorted by phone number.

9) Make a WordStream class that takes a stream of characters and produces a stream of words. A word is a string with no white space or punctuation.

aStream := WordStream on: 'resume.tex' asFilename readstream.
aStream next


will return the first word in the file 'resume.tex'.

WordStream should be a subclass of Stream. Assume the input stream of characters might be very big. In other words, do NOT try to read it all in and break it into words, but find the next word in the input stream each time the WordStream receives the #next message.

10) Use WordStream to write a script that counts the words in a document (use a bag) and prints out a report that tells you the number of times each word appeared in the document. The report should sort the words, most popular first. Make the script be a class method of WordStream.

11) Use WordStream to write a script that finds all dollar words in a document. A dollar word is a word that is worth 100 points. The points in a word is the sum of the points of its letters, and 'a' is worth 1 point, 'b' is worth 2 points, etc. This was a homework in my daughter's second grade class. She spent a week on it. Too bad she didn't know how to program!

12) (This is hard!) Streams can be used for backtracking. Make a subclass of Stream called "QueenStream" that you can use to solve the 8 Queens problem, or any number of Queens. The problem is figuring out how to place 8 queens on an 8 x 8 chess board such that none of them are attacking any of the others. Each instance of QueenStream will keep track of the position of one queen. It is a stream because it gives a sequence of legal board positions for itself and the queens less than it. In other words, it is a generator of legal board positions.

The generator of positions for queen N assumes that the queen is on column N, because there must be one queen for each columns. It will have a generator (i.e. an instance of QueenStream) for the queens on the columns before it. It has a description of the current position of the board. And it knows N, which is its column.

Make a class Board that keeps track of the positions of the queen on each column. You'll find you need operations to initialize the board, to move the queen on a column forward one row, to test whether the queen on one column is at the last row, and to test whether the queen on one column attacks any of the others. A queen attacks another if it is on the same row, or on the same diagonal.

A QueenStream is at the end when its input stream is at the end and its queen is in the last row. If a QueenStream's queen is at the last row then it gets its next board position by getting the next board position from its queen stream. Otherwise, it gets it by moving its queen to the next row.

When you initialize a QueenStream to column 1, it will get a starting board position and an empty stream as a generator; because once it moves the queen in column 1 to the end, it can't backtrack. When you initialize a Queen to a column c, it will get a get a QueenStream for column c - 1 and select all board positions from it that are legal.

Fix Documentation