Print Story Help me
Help!
By jacob (Wed Mar 10, 2004 at 02:01:35 AM EST) (all tags)
for free.

This is a draft of my article about Scheme targeted for K5. There are things I don't like about it, and it will be edited, probably heavily, before I submit it, but I thought maybe people would like to look it over and tell me whether they think the technical argument is comphrehensible, what you like and don't like and what you'd like to see more of, what's confusing, et cetera. I'll probably be changing the conclusion and possibly adding more material. I've been flip-flopping about whether I should present things as a large example (as I have in this incarnation) or as a series of small examples and experience reports, so if you told me what you thought of that aspect of the presentation I'd appreciate it.

The audience is supposed to be mainstream programmers who know some C or Java or something like that.



Why I Like Scheme
Draft - 3/10/04

When I tell programmers that my favorite programming language is Scheme, they tend to react as though I'd told them my favorite snack were packing styrofoam or my favorite music were junior high orchestra recordings: those who can even bring themselves believe me mostly wonder what on Earth I could be thinking. Some wonder if I've ever programmed in a real language like C or C++ (I have), if I'm one of those academic nuts who cares more about proving theorems about programs than actually using them for real work (I'm not), or if all those parentheses have rotten my brain (probably). So what could make me like this weird language with no for loop and so many parentheses it makes you crosseyed?

In this article, I'll tell you what, and by the time I'm finished I hope I'll have convinced you that you might want to give it a shot yourself. I'm going to assume that you're a working programmer who comes from a C, C++, Java, or similar background, so I'm not going to go into the details of how Scheme differs from other more-or-less exotic languages like Common Lisp, Standard ML, or Haskell, as interesting and worthy of discussion as those languages are. Suffice it to say that many of the ideas I present here as features of Scheme are also present in some other languages.

That disclaimer out of the way, let's get on to the good stuff.

Scheme head-first

I'm not going to try to teach you how to program in Scheme, but instead to give an impression of how a Scheme programmer tackles problems. So, rather than showing some boring pedagogical example like "Hello world" or a factorial function, I figured I'd show something I've actually had occasion to use in real work: a multithreaded port scanner. Here it is, written basically the way I'd really write it if I needed to:


; scan : string[hostname] int int -> listof (list int string)
; produces a list of all open TCP ports on the given host and their
; associated well-known service names, listed in order from low to high
(define (scan host low high)
  (build/threaded port (from low to high)
     (if (can-connect? host port)
         (list port (port->name port))
         #f)))

See, that's not so bad. The first three lines are comments describing the function; naturally they're not necessary for the program to run. The next defines a function called scan which takes three arguments: host, low, and high. Conceptually we'd like scan to return an ordered list of all the ports that are open on the given host, where all the decisions about each particular port were made in parallel with each other. That sounds complicated, so with our Scheme hats on we'll just make up some new syntax called build/threaded that does it for us and worry about how that syntax works later. We'll say that you give it the name of a new variable (port in this case), specify a range using a for ... to ... construct, and give it a body of code to execute, and it executes the body in parallel for each number in the range and returns an ordered list of all the results. And just because we want to make our lives really simple, we'll say that output list only includes a result if the body didn't return #f (false).

Assuming we've got build/threaded, implementing scan is easy. The body of the loop just needs to test if the given port is open (we'll write a function can-connect? to check that) — if it is we'll return a list containing the port number and the well-known service name (we'll need to write port->name to do that), and if it's closed we return false (in Scheme, the body of a function is an expression, not a statement, and whatever that expression evaluates to is implicitly considered the function's return value).

That's the whole overall program. All that's left is to fill in the details for can-connect?, port->name, and build/threaded.

The details

The can-connect? function isn't bad at all:


; can-connect? : string[url] number -> bool
; determines if the host is listening on the given port
(define (can-connect? host port)
  (with-handlers ([exn:i/o:tcp? (lambda (e) #f)])
    (let-values ([(ip op) (tcp-connect host port)])
       #t)))

It's just a small wrapper around the tcp-connect library function, which returns two values, an input port and an output port, if it succeeds at connecting, and raises an exception if it doesn't. PLT Scheme has a built-in exception mechanism very similar to Java's or C++'s in that any code can raise an exception, and that exception propagates up the stack until it reaches an appropriate handler (specified by try/catch in C++ or Java, with-handlers in PLT Scheme). The can-connect? function calls tcp-connect and returns #t (true) if it succeeds. If tcp-connect raises an i/o:tcp exception (which it will do if it can't establish a connection), with-handlers catches that and returns #f (false).

The port->name function is a little more complicated. If I only wanted my program to run on Unixish systems I might just call the getservbyport C function through PLT Scheme's foreign function interface (it only takes a few lines, actually), but I'd actually like it to run on Windows and Macs and various other platforms that don't have access to getservbyport (or even the /etc/services file it uses). Fortunately, the Internet Assigned Numbers Authority has an online list of ports in the same format as /etc/services, so we'll make our program fetch that if a local file isn't available. Here's the code:


(define NAMES
  (let ([ip (if (file-exists? "/etc/services")
                (open-input-file "/etc/services")
                (get-pure-port (string->url "http://www.iana.org/assignments/port-numbers")))]
        [nametable (make-hash-table)])
    (while m (regexp-match "([^ \t\r\n])[ \t]+([0-9]+)/tcp[ \t]+([^\r\n])" ip)
       (hash-table-put! nametable (string->number (list-ref m 2)) (list-ref m 1)))
    nametable))

(define (port->name p) (hash-table-get NAMES p (lambda () "unknown")))

First we define the global variable NAMES to be a hash table that maps numbers to strings. To do that, we get a new input port that's connected to either /etc/services (if it exists) or to the IANA's web page, and we also make an empty hash table. Then we loop over the input port getting each port-number/name combination (as determined by a regular expression) and add each to the hash table, and finally we return the hash table which becomes the value of NAMES. Unfortunately Scheme has no while loop, but since it's such a nice fit for this problem we'll use it anyway and remember to define it later.

Once we've got NAMES defined port->name is just a hash-table lookup with the string "unknown" returned if the port we're looking for isn't in the table. That completes the function, so all we have left to do is define the loops we used.

Macros and concurrency

We can't put it off any longer: we've got to make definitions for while and build/threaded. The former is easier, so we'll do it first.

The key to making new syntax forms is a very unusual facility called macros (only a cousin of C macros, the well-known black sheep of the family) that take a piece of a parse tree as input and produce another one as output, allowing programmers to automatically rewrite code as they see fit. Here's the macro for while:


(define-syntax (while stx)
  (syntax-case stx ()
    [(_ var test body)
     (identifier? #'var)
     #'(let loop ((var test))
         (when var body (loop test)))]))

It's scary-looking, but don't fear, it's really pretty simple once you get used to the way macros are written. There's some junk at the top that says that this is a macro and sets us up to do some work, but the important stuff begins on the third line, which specifies what shape the syntax tree should take. It should be

(while [some-variable] [some-test] [some-body])

where the variable has to be a literal identifier but the test and the body can be any old Scheme expression. It also gives each piece a name (var, test, and body, respectively). Those names are important for the rest of the macro: starting with the #'(let ...) line, the whole rest of the code is actually a template for what the output syntax should be, and it's returned literally except that every time the name of one of the blanks appears, whatever actually appeared in that blank is substituted in instead.

Once you get the hang of it, macros like these are pretty simple. For example, our use of while in defining the NAMES table turns into the following after Scheme expands it (code from the original scan in bold, code introduced by the macro in normal font weight):


(let loop ((m (regexp-match "([^ \t\r\n])[ t]+([0-9]+)/tcp[ \t]+([^\r\n])" ip)))
  (when m
    (hash-table-put! nametable (string->number (list-ref m 2)) (list-ref m 1))

    (loop (regexp-match "([^ \t\r\n])[ t]+([0-9]+)/tcp[ \t]+([^\r\n])" ip))))

Of course we won't actually ever see this code or type it in — Scheme generates it for us behind the scenes when it runs our program. What the expanded code actually does is use one of Scheme's built-in loops, the so-called "named let", to run the test expression and bind it to the variable m. If m isn't false (in Scheme anything that isn't #f is true), it evaluates the body with m still bound to the test's result and then loops, checking the test again. It's a simple pattern, and one that would be hard to write down in other languages. People from imperative backgrounds often criticize Scheme for making you use recursion to express looping, but that's missing the point: recursion is the "assembly language" for loops in Scheme, but (in addition to a large library of prebuilt loops) the language gives you the tools you need to easily build up the most powerful flow-control abstractions you can think of.

The last loop we need to implement to complete our program demonstrates that point nicely. Remember we needed to make build/threaded, a flow construct that lets you specify a range and a body and builds a list of all the non-false evaluations of that body on the given range in parallel. As it happens, for another project I once implemented a nice library function called threaded-for-each that takes a function and a list and calls the function on every element of the list in parallel. It looks like this:


(define (threaded-for-each f lst)
  (let ((chan (make-channel)))
    (for-each
     (lambda (x) (thread (lambda () (f x) (channel-put chan 'done))))
     lst)
    (for-each (lambda (ignored) (channel-get chan)) lst)))

You may notice that the thread interface is unusual. Rather than the traditional semaphore/mutex lock system, PLT Scheme uses unusual concurrency mechanisms taken from Concurrent ML to create and synchronize threads. Rather than locks on shared memory, the CML primitives allow threads to send values to each other over channels and block until those values are transmitted. In this example, threaded-for-each creates a new channel, spawns a thread for each item in the input list that runs the input function on it and then sends a message over the channel indicating that it is done. Once all threads are spawned, the function waits for a message from each of the spawned threads and then returns.

We can use threaded-for-each to implement build/threaded. Here's an implementation:


(define-syntax (build/threaded stx)
  (syntax-case stx (from to)
    [(_ x (from start to end) body)
     (identifier? #'x)
     #'(let ([v (make-vector (- hi lo) #f)]
             [i start]
             [j end])
         (threaded-for-each
          (lambda (x)
            (let ((ans body))
              (when ans (vector-set! v (- x i) ans))))
          (range i j))
         (filter (lambda (y) y) (vector->list v)))]))

This is the biggest chunk of Scheme code in the program, and it's a macro too, so you know it's got to do something cool. Here's how it works: we create a vector that has a slot for every element in the target range, all initialized to #f. Then we make a list of all the numbers in our range and use threaded-for-each with a function that runs the macro's body on each number in that list, updating the appropriate cell in the vector when it's finished. Finally, once the threaded-for-each is over with (and therefore all of its threads have completed) we turn the vector into a list, strip out all the #fs, and return it.

Some conclusions

That's the whole port scanner. The interesting thing about this solution isn't its size or efficiency, but how at nearly every stage of the process Scheme gave me the ability to say exactly what I meant, so I could make the language do things my way rather than it making me do things its way. Schemers take full advantage of that ability, and its hard to find a group of programmers more stubborn in their pursuit of writing down exactly the concept they are trying to convey rather than having to clutter their programs with extraneous concepts.

< Curses! Foiled again! | BBC White season: 'Rivers of Blood' >
Help me | 21 comments (21 topical, 0 hidden)
OK by hulver (6.00 / 1) #1 Wed Mar 10, 2004 at 02:14:19 AM EST
First paragraph
those parentheses have rotten my brain
Should be rotted

Didn't see any others. Nice article.
--
Cheese is not a hat. - clock

fixed on my local copy by jacob (3.00 / 0) #2 Wed Mar 10, 2004 at 02:25:59 AM EST
Thanks.

--

[ Parent ]
Looking pretty good by gazbo (3.00 / 0) #3 Wed Mar 10, 2004 at 02:29:03 AM EST
One thing that stood out to me, however, was the identifier build/threaded.  I'm fairly sure from context that the / is just another character in the identifier (ditto for the i/o later on) but to those of us not used to it, it looks like a language construct.

Assuming I'm right, maybe a brief note to mention that?  Or maybe people are capable of inferring it themselves.


I recommend always assuming 7th normal form where items in a text column are not allowed to rhyme.

yeah, I'll note that by jacob (3.00 / 0) #8 Wed Mar 10, 2004 at 02:49:32 AM EST
It's funny, to my eyes that sort of thing doesn't even look unusual anymore. Scheme is much more lax about what can be an identifier due to its lack of attempt at complex syntax; +, *, &, ^, % and even things that are ordinarily keywords like if or lambda are all legal identifiers.

--

[ Parent ]
"real work" is paid work by Rogerborg (3.00 / 0) #4 Wed Mar 10, 2004 at 02:30:26 AM EST
So you might want to explain how it's possible to make a living out of Scheme.  As far as I can see, it's just another tool for writing throwaway toys like Scoop rating bots.  

Sure, C sucks, but until I see an embedded device SDK that requires another language, it's what I'm sticking to.  Can you persuade me that there's tangible value in learning Scheme?

-
Metus amatores matrum compescit, non clementia.

hmmm by jacob (6.00 / 1) #7 Wed Mar 10, 2004 at 02:44:24 AM EST
Well, Scheme isn't really suited for device drivers and low-level stuff like that in my opinion (there are people who'd disagree with me). That said, I do occasionally make money programming in Scheme, so maybe I should add a little discussion of that. Thanks for the feedback.

--

[ Parent ]
good article by phred (3.00 / 0) #5 Wed Mar 10, 2004 at 02:31:37 AM EST
but it also highlights one of my problems with much programming, general unreadability. This isn't detracting from your article, just a favorite gripe of mine.

In all fairness, the tone of your article is probably fine for top notch programmers, and for the rest of the world, we can probably get help from the web.

A particular note is your explanation of the syntax making macro (?) didn't sink in after a few readings, but then again I haven't done much unixy programming besides hobby level c and perl (ie, nothing industrial strength).

Still a real cool article and I'm going to try to understand more of it as time allows.


macros by jacob (3.00 / 0) #6 Wed Mar 10, 2004 at 02:33:49 AM EST
Yeah, I've been struggling with making that part clear. It needs more work.

Thanks for the feedback!

--

[ Parent ]
hope you can dumb it down for me hehe by phred (3.00 / 0) #9 Wed Mar 10, 2004 at 03:42:01 AM EST
I'd especially love to do sockets in scheme, as AI is an especially appealing paradigm for trolling on IRC^w^w^w research.

[ Parent ]
can-connect? bug by ENOENT (3.00 / 0) #10 Wed Mar 10, 2004 at 03:54:46 AM EST
can-connect? never closes the tcp ports returned by tcp-connect.

Life is just one damned thing after another.
Love is just two damned things after each other.


good eye! by jacob (3.00 / 0) #11 Wed Mar 10, 2004 at 04:19:41 AM EST
I had done that deliberately because I was thinking they'd get shut down automatically when the program ended, but now that you mention it I realize that could be a while if the scan function is used in another context. I'll fix it. Thanks!

--

[ Parent ]
Also... by ENOENT (3.00 / 0) #13 Wed Mar 10, 2004 at 04:47:48 AM EST
Your average OS will not be happy with a process that has 65535 open file descriptors, in the case of scanning all ports.


Life is just one damned thing after another.
Love is just two damned things after each other.


[ Parent ]
language flamebait by ucblockhead (3.00 / 0) #12 Wed Mar 10, 2004 at 04:30:10 AM EST
The article is good, but let me play devil's advocate a bit. If I'm a C programmer looking at that, one of my reactions is to wonder why the language doesn't include a "while" loop as part of the base language, given that such a simple example required you to build one.

While it is extremely nice to be able to build special purpose loops like your threaded for loop, it seems that if a construct is used in nearly every program, then a language built-in would be more efficient, and would avoid the issue of every programmer having their own "while" implementation, some of them buggy.

It's like C++ strings in the eighties. (And to some extent, today.) Sure, you can build up a string class that does the work, but I think it's pretty obvious that the language would have been better with string builtins.
---
[ucblockhead is] useless and subhuman

Scheme and while by jacob (3.00 / 0) #14 Wed Mar 10, 2004 at 05:16:56 AM EST
The thing about loops like these is that it quickly becomes a maze of twisty passages all alike. For instance, my while loop isn't really C's while loop, it's more like a while (x = ...) { } loop that additionally declares x. There are a million little variations of that, and all of them can be easily expressed as a standard recursion or named-let loop. Furthermore, the only kind of looping that this sort of construct is good for (as opposed to the many built-in loops Scheme provides) are loops that hinge on something being repeatedly mutated until some condition that can't be determined by the data's structure is reached. That's a pretty unusual situation to be in in a language like Scheme, where most programs don't ever do anything like that.

So basically I'd guess it's not in the standard because it just doesn't come up that much in Scheme.

--

[ Parent ]
Code critique by dilap (6.00 / 1) #15 Wed Mar 10, 2004 at 11:08:14 AM EST
First, some typo-ish stuff:

* When I try to run this w/ a fresh install of DrScheme, it complains  thusly: "reference to undefined identifier: get-pure-port"

* Similar complaints for range and filter; I could get filter working  by "(require (lib "list.ss"))"

* Is vector-set! thread safe?

* I think "(make-vector (- hi lo) #f)" in build/threaded should be  "... (- end start)", maybe w/ a (+ 1 ...)

You should definitely provide information on how to run the code in your final article.

Deeper stuff:

If (scan host low high) is clear, then why isn't (build/threaded port (low high)) clear? It seems you're defining syntax ("from ... to ...")  simply for the sake of syntax.

But beyond that, I think build/threaded conflates three distinct things: (1) creating a range of numbers, (2) mapping a function over a sequence threadedly, and (3) filtering a sequence

Obviously, (3) is already built into (Dr)Scheme; (1) should be (but isn't?). Your main contribution is (2). Since mapping is a more general operation than map-and-filter -- practically every functional language provides map and filter, but none (as far as I know) provide "build" -- it makes more sense, in my opinion, to add map-threaded.

Similarly, (but less significantly for this example) I think scan should take a list of ports, rather than a range. (EG, what if you want to check ports 20, 21, 80, and 480? It would be silly to have to check all the ports between 20 and 480.)  Note that, with a good range primitive, it's still no harder to check all the ports between 20 and4 80: (scan (range 20 480)) ; vs. (scan 20 480)

I should probably go do some homework. . . .

(Excellent article, btw.)


will reply more later by jacob (3.00 / 0) #16 Wed Mar 10, 2004 at 11:15:36 AM EST
but just to address the "getting the code working" part: yes, I left out a couple details (I'm glad somebody caught that!). For one thing, you have to (require (lib "url.ss" "net")) for get-pure-port as well as the list.ss require you found. For another, you've got to put all the macros at the top of the file in DrScheme unless you want to package things up in modules. Finally, I think I just forgot to give the definition of range. Here it is:


(define (range i j)
    (cond
      [(>= i j) '()]
      [else (cons i (range (+ i 1) j))]))

I'll have another reply later, but I wanted to at least address how to make the code work.

--

[ Parent ]
No rush by dilap (3.00 / 0) #18 Wed Mar 10, 2004 at 11:25:51 AM EST
I've got a lab in 40 minutes until late, and then (hard!) homework due the next morning, so I shouldn't be doing this anyway. . . .

Your range code reminded me of another thing: You should make special note of the fact that

(if cond
    a
    b)

is (in Cish)

if(cond) { a; } else { b; }

NOT

if(cond) { a; b; }

which is what it looks like to a Cish programmer.

(if i were a superhero, my superpower would be procrastination),

dilap

[ Parent ]
Code by dilap (3.00 / 0) #17 Wed Mar 10, 2004 at 11:16:04 AM EST
Because I like flunking out of physics.

(define (scan host ports)
  (filter (lambda (x) x)
          (map-threaded-ordered (lambda (port)
                                  (and (can-connect? host port) (list port (port->name port))))
                                ports)))

;; real range primtive should be more flexible (cf. python)
(define (range a b) ; [a,b), i.e., non-end-inclusive
  (if (>= a b) '() (cons a (range (+ 1 a) b))))

(define (map-threaded f lst) ; order of result undefined
  (let ((chan (make-channel)))
    (for-each (lambda (x) (thread (lambda () (channel-put chan (f x))))) lst)
    (let loop ((done '()) (todo lst))
      (if (null? todo) done
          (loop (cons (channel-get chan) done) (cdr todo))))))

(define (map-threaded-ordered f lst)
  (letrec ((n (length lst))
           (v (make-vector n #f)))
    (map-threaded (lambda (i-x) (vector-set! v (car i-x) (f (cadr i-x))))
                  (map (lambda (i x) (list i x)) (range 0 n) lst)) ; aka zip
    (vector->list v)))

(requre (lib "list.ss")) ; for filter

[ Parent ]
ok by jacob (3.00 / 0) #19 Wed Mar 10, 2004 at 12:37:47 PM EST
* Is vector-set! thread safe?

It is in the sense that multiple threads can update different cells in the same vector simultaneously, which is all we need here.

* I think "(make-vector (- hi lo) #f)" in build/threaded should be  "... (- end start)", maybe w/ a (+ 1 ...)

Almost. I screwed up, but your fix neglects the possibility that start and end could be side-effecting expressions that should only be evaluated once (that's the point of binding them to i and j in the next line). It should be


#'(let ([i start]
        [j end])
    (let ([v (make-vector (- j i) #f)])
       ...))

You should definitely provide information on how to run the code in your final article.

Agreed.

If (scan host low high) is clear, then why isn't (build/threaded port (low high)) clear? It seems you're defining syntax ("from ... to ...")  simply for the sake of syntax.

Yes, I put in the from ... to ... construct basically entirely to demonstrate that I'm really making up new syntax rather than writing a function or something. I also had no other macros when I wrote that (I added the service name lookup and thus the while macro later), which is why I chose to make it a macro at all rather than a function, which is what I'd do for myself. Maybe I should revisit that decision; I think the while macro is mind-bending enough for people who've never seen syntactic abstraction. I'll have to think about it more.

But beyond that, I think build/threaded conflates three distinct things: (1) creating a range of numbers, (2) mapping a function over a sequence threadedly, and (3) filtering a sequence

Obviously, (3) is already built into (Dr)Scheme; (1) should be (but isn't?). Your main contribution is (2). Since mapping is a more general operation than map-and-filter -- practically every functional language provides map and filter, but none (as far as I know) provide "build" -- it makes more sense, in my opinion, to add map-threaded.

I was making the code a little bit more imperative as a way of making it a little more comfortable to people with no functional programming background. In fact I originally implemented threaded-for-each as a helper function for threaded-map. :) Maybe I'll change it back that way, particularly because I was definitely ambivalent about your next point:

Similarly, (but less significantly for this example) I think scan should take a list of ports, rather than a range. (EG, what if you want to check ports 20, 21, 80, and 480? It would be silly to have to check all the ports between 20 and 480.)  Note that, with a good range primitive, it's still no harder to check all the ports between 20 and4 80: (scan (range 20 480)) ; vs. (scan 20 480)

Again this was a concession to make the code more like code that imperative programmers have likely seen, but I went back and forth about whether I should build a list anyway for exactly the reasons you described. I'll probably change that back, actually.

I should probably go do some homework. . . .

Of course not!

Thanks very much for your comments.

--

[ Parent ]
Good job! (And my notes.) by tmoertel (6.00 / 1) #20 Wed Mar 10, 2004 at 05:27:31 PM EST
First, I like it. It's conversational and keeps the reader moving forward as increasingly complex topics are revealed.

A few proofreading notes:


--
Write Perl code? Check out LectroTest. Write markup-dense XML? Check out PXSL.

Since you doubtless wanted to see it in Haskell by tmoertel (3.00 / 0) #21 Wed Mar 10, 2004 at 07:37:33 PM EST
Since I know you can't resist looking:

module Main (main) where

import Control.Concurrent
import Control.Exception as Ex
import IO
import Network
import Network.BSD
import System

main :: IO ()
main = do
    args <- getArgs
    case args of
        [host, from, to] -> withSocketsDo $
                            portScan host (read from) (read to)
        _                -> usage

usage = do
    hPutStrLn stderr "Usage: Portscan host from to"
    exitFailure

portScan :: String -> Int -> Int -> IO ()
portScan host from to = do
    results <- mapM (threadWithChannel . scan1Port host) [from .. to]
    mapM_ printHits results
    where
    printHits mvar = do
         result <- takeMVar mvar
         case result of
             Just port -> withDefault (show port) (lookupService port)
                          >>= putStrLn
             _         -> return ()

threadWithChannel :: IO a -> IO (MVar a)
threadWithChannel io = do
    mvar <- newEmptyMVar
    forkIO (io >>= putMVar mvar)
    return mvar

scan1Port :: String -> Int -> IO (Maybe Int)
scan1Port host port =
    withDefault Nothing (tryPort >> return (Just port))
    where
    tryPort = connectTo host (PortNumber (fromIntegral port)) >>= hClose

lookupService :: Int -> IO String
lookupService port = do
    service <- getServiceByPort (fromIntegral port) "tcp"
    return (show port ++ " " ++ serviceName service)

withDefault :: a -> IO a -> IO a
withDefault defaultVal io =
    handle (const $ return defaultVal) io

-- Local Variables:  ***
-- compile-command: "ghc -o Portscan --make Portscan.hs" ***
-- End: ***

Example usage:

$ ./Portscan redwood 1 500
21 ftp
22 ssh
80 http
111 sunrpc
139 netbios-ssn
443 https

Thanks for the mini-PFC!

--
Write Perl code? Check out LectroTest. Write markup-dense XML? Check out PXSL.

Help me | 21 comments (21 topical, 0 hidden)