![]() |
Ports and Stuff! |
In this lab you will learn how to open and close files and connect strings as well as files to ports so you can get data bit by bit.
What the hell are ports?
Ports are basically a way to turn some sort of data that
may be complex or otherwise not easy to play with into
something managable and straightforward.
Why the word "port?" Well, frankly we call them ports because it's a real-world metaphor to which most people can relate.
Boats? Sure, why not. Imagine you work at a shipyard. You have to deal with all these boats out in the sea, and somehow you need to be able to get things on and off the boats. Not only that, but maybe if you're trafficking something like illegal drugs or cars, you want to make sure the movement of goods goes exactly as you want. To do this, you need to control the inventory as it comes on and off the boats.
Here's a rough picture of a shipyard:
(ocean)
+--o---o---o---o---o---o--+
| 1 2 3 4 5 6 | (ports)
+--^---^---^---^---^---^--+
(traffic control)
O
\+/
|
/ \
(you)
(warehouse)
/----\
------
| |
/------\
On one side you have the ocean (where the boats are) and
on the other there is your warehouse.
Say you want a ship to dock so you can get some drugs or cars off the boat. You tell traffic control you want the boat to dock, and it picks a free port and attaches your desired boat to that dock:
+-+
| |
| |
V
+--o---o---o---o---o---o--+
| 1 2 3 4 5 6 | (ports)
+--^---^---^---^---^---^--+
As you can see, the port is narrow so you can only get
one thing off the boat at a time. Now that it is docked
at port 1, you can tell your minions to get stuff from
that port:
"Hey Bob, go see what's at port 1."So Bob goes to the port and isn't allowed onto the boat but that's okay since he can "peek" in and see what's first in line to come out. Bob sees what it is and comes back to tell you.
"Okay Sid."
"Hey, Sid, it was a Porsche."Bob goes back to the boat and takes the first car (which was a Porsche) and puts it in the warehouse. He then goes back to the boat repeatedly until there are no cars left. He pushed a few into the ocean and put a few in the warehouse, but there are no cars left on the boat. When he goes back to the boat, this time instead of a car he is given an End Of Freight slip.
"Okay, that means there are cars on that boat. Why don't you go get the cars one at a time. If it is a foreign car, put it in the warehouse, if it's a domestic then push it into the ocean."
"Hey, Sid, boat's empty. Here's the EOF."You leave and go back to the office. You hand your boss the key and say:
"Thanks, now close the port so the boat can leave."
"Hey Alex, here's the key to the warehouse. I filled it with the cars you wanted."You go home and that's the end of the story.
"Thanks Sid. Go home."
You're probably wondering what that had to do with scheme. It's quite simple really:
Then you can see what the first thing waiting at the port happens to be:> (define myport (open-input-string "a bunch of cars")) > myport #<input port string>
Then go to that port to get stuff:> (peek-char myport) #\a
And so on until you read the end:> (read-char myport) #\a > (read-char myport) #\space
That's kind of like the "end of freight" slip that Bob gave to you. Now there's an easy way to figure out when you are done.> (read-char myport) #!eof > (eof-object? (read-char myport)) #t
When you get these objects you can do whatever you want with them: put them in a list, throw them into the ocean, etc.
When you're done with the port, you close it to let the boat go away by using (close-input-port <input-port>).
Okay, let's pretend that our boat is the following string:
Now, Bob needs to get all the imports and put them in the warehouse and throw all the domestics away (ignore them). Here's a scheme program that basically does what Bob did:> (define boat "ididddidiiiidid")
(define do-work
(lambda (boat)
(let ([port (open-input-string boat)])
(if (char? (peek-char port)) ;;see if there are cars
(let bob ([warehouse '()]) ;;warehouse starts empty
(let ([next-car (read-char port)])
(if (eof-object? next-car)
(begin (close-input-port port)
warehouse) ;;return warehouse
(if (equal? #\i next-car)
(bob (cons next-car warehouse))
(bob warehouse))))))))) ;; throw it away
> (do-work boat)
(#\i #\i #\i #\i #\i #\i #\i #\i)
I'll be darned, the warehouse had a bunch of boats in it!
Notice how I determined that the end of the file showed up?
I used (eof-object? <object>) to check if the last thing
read was the "end of freight" slip.
You can do this with files too. The difference is the way you open the file: you need to change the part of the work procedure that opens the input string to:
Where <filename> is the name of the file!(open-input-file <filename>)
(define do-work-on-file
(lambda (boat-file-name)
(let ([port (open-input-file boat-file-name)])
---)))
Nothing else changes! I dare you to try this out on this file!
More about file IO to come...
Before you start working with files, you need to know the following tools:
The procedure cd does two things:
> (cd "c:/WINNT")
> (cd) "c:/WINNT"
To open a port that you can read from, use the procedure (open-input-file <filename>). It will throw an error if the file doesn't exist. You can then read characters from it using peek-char and read-char or any other procedures you define.
(peek-char <port>) (read-char <port>) (unload-boat <port>) ;;I made this up
To open a port to which you can write use the procedure (open-output-file <filename>). Scheme will open the file specified. If it doesn't exist, it will be created if it does exist open-output-file will throw an error.
If you want to open a file that already exists you have two choices: replace it completely or add to the end. To do one of these use either of the following:
'replace tells scheme to overwrite what was there, and 'append (you guessed it!) tells it to add to the end.(open-output-file <filename> 'replace) (open-output-file <filename> 'append)
To write data to a file, you can do pretty much the same thing you did to read: use the write-char procedure. (write-char <char> <output-port>)
Just like a shipyard, there are only a limited number of ports that can be used. If you don't close them, you can't have more boats pull in! You could get an unexpected error if you keep opening files but not closing them and you run out of ports.
Also, when you close the port any writing that you did gets finalized. Until then it is not guaranteed to completely be in the file. Scheme keeps a buffer of what you are writing to the file so that it can do the writes in chunks which is much faster. When you close the port, scheme finishes writing completely and cleans up a bit.
In general, when you're done with a file, CLOSE THE PORT!
So that's pretty much it. You can read and write characters now. It's time to extend them so you can do some more useful things!
Define a predicate digit? that takes a character and returns #t if it is a number character: #\0, #\1, ... #\9.
(define digit?
(lambda (c)
----))
> (digit? #\f)
#f
> (digit? #\space)
#f
> (digit? #\3)
#t
HINT: use char->integer.
HINT2: I will send you to the penalty box if you if you use magic numbers (for example, the number 57 or 48)> (char->integer #\c) 99 > (char->integer #\0) 48 > (char->integer #\1) 49 > (char->integer #\9) 57
(define digit?
(lambda (c)
(if (char? c)
(<= (char->integer #\0)
(char->integer c)
(char->integer #\9))
(error 'digit? "~s is not a character!" c))))
Next, define digit->integer that converts a character into its integer value.
(define digit->integer
(lambda (d)
----))
> (digit->integer #\2)
2
> (digit->integer #\9)
9
(define digit->integer
(lambda (d)
(- (char->integer d)
(char->integer #\0))))
Use your previous two answers to define a procedure read-int that takes an input port and reads the first number from the port. For example:
> (define p (open-input-string "123 3")) > (define p2 (open-input-string "x14")) > (define p3 (open-input-string "14x45 b")) > (read-int p) 123 > (read-int p2) 0 > (read-int p3) 14
Do not throw an error if the open port doesn't start with an integer. It will be MUCH harder to complete the rest of the exercises if you do this.
HINT: Stop reading the data once you encounter a non-digit.
HINT2: Use Horner's rule to figure out the integer. Horner's rule basically tells you how to figure out what the number is:
132 = 100 + 30 + 2
15892 = 10000 + 5000 + 800 + 90 + 2
(define read-int
(lambda (ip)
(let loop ([acc 0] [c (read-char ip)])
(if (or (eof-object? c) (not (digit? c)))
acc
(loop (+ (* 10 acc) (digit->integer c)) (read-char ip))))))
;this solution has this behavior:
> (define p (open-input-string "20x4b"))
> (read-int p)
20
> (read-int p)
4
> (read-int p)
0
> (read-int p)
0
(define read-int
(lambda (ip)
(let loop ([acc 0])
(if (or (eof-object? (peek-char ip))
(not (digit? (peek-char ip))))
acc
(loop (+ (* 10 acc)
(digit->integer (read-char ip))))))))
;this one has this behavior (will never reach #!eof for p):
> (define p (open-input-string "20x4b"))
> (read-int p)
20
> (read-int p)
0
> (read-int p)
0
The problem with this is that it will never get past a delimiter.
A workaround would be to read through a stream until you get a number:
(define read-int
(lambda (ip)
(let loop ([acc 0] [reading #f])
(cond
[(eof-object? (peek-char ip))
(if reading acc #!eof)]
[(digit? (peek-char ip))
(loop (+ (* 10 acc)
(digit->integer (read-char ip)))
#t)]
[reading acc]
[else (read-char ip) (loop acc #f)]))))
;this one has this behavior (like read does):
> (read-int p)
20
> (read-int p)
4
> (read-int p)
#!eof
Now, define procedure read-int-signed that will read an integer as in exercise 3, but it will properly read a stream that starts with a + or - as well! For example:
> (read-int-signed (open-input-string "123 3")) 123 > (read-int-signed (open-input-string "+14")) 14 > (read-int-signed (open-input-string "-14x45 b")) -14 > (read-int-signed (open-input-string "x15ba")) 0 > (read-int-signed (open-input-string "")) 0
(define read-int-signed
(lambda (ip)
(cond
[(equal? (peek-char ip) #\-)
(read-char ip) ;;throw away first char
(- (read-int ip))]
[(equal? (peek-char ip) #\+)
(read-char ip) ;;throw away first char
(read-int ip)]
[else (read-int ip)])))
If read-int is defined as the third solution above, use this:
(define read-int-signed
(lambda (ip)
(cond
[(eof-object? (peek-char ip)) ;;needed because of recursion
#!eof]
[(equal? (peek-char ip) #\-)
(read-char ip) ;;throw away first char
(let ([n (read-int ip)])
(if (eof-object? n) ;;have to check this now!
n
(- n)))]
[(digit? (peek-char ip)) ;;see if we're starting with a digit
(read-int ip)]
[else ;;no sign, go on
(read-char ip)
(read-int-signed ip)])))
I was able to eliminate the + case since "+23" and
"x23" will give the same result. It just skips over the +.
But there's still a bug in read-int-signed! What about "-"? (This is a lot of trouble to fix that one bug) Here's my thought process:
(define read-int-signed
(lambda (ip)
(cond
[(eof-object? (peek-char ip))
#!eof]
[(equal? (peek-char ip) #\-)
(read-char ip)
(cond
[(eof-object? (peek-char ip)) #!eof] ;; Have to add this check!
[(digit? (peek-char ip)) ;; Have to add this check!
(let ([n (read-int ip)])
(if (eof-object? n)
n ;; We can't negate #!eof !!!
(- n)))]
[else
(read-int-signed ip)])] ;; Try again! ("--2" => -2)
[(digit? (peek-char ip))
(read-int ip)]
[else
(read-char ip)
(read-int-signed ip)])))
This is a complex problem to figure out all the details, so I will
definitely be happy if you came up with any of the other answers. :)
Why didn't I put (peek-char ip) into a let binding? Well, peek-char is a little tough to work with and depends on when you use it (after a read it will be different) so I elected to just hard-code it. This would work too:
(define read-int-signed
(lambda (ip)
(let ([first (peek-char ip)])
(cond
[(eof-object? first)
#!eof]
[(equal? first #\-)
(read-char ip)
(cond
[(eof-object? (peek-char ip)) #!eof] ;; Have to add this check!
[(digit? (peek-char ip)) ;; Have to add this check!
(let ([n (read-int ip)])
(if (eof-object? n)
n ;; We can't negate #!eof !!!
(- n)))]
[else
(read-int-signed ip)])] ;; Try again! ("--2" => -2)
[(digit? first)
(read-int ip)]
[else
(read-char ip)
(read-int-signed ip)]))))
Notice some calls to peek-char are still there. This is because
once first is bound, its value doesn't change. peek-char changes if
I read something off the stream:
> (let ([p (open-input-string "abc")])
(let ([first (peek-char p)])
(read-char p)
(equal? first (peek-char p))))
#f
phew!