Tuesday, August 18, 2009

Posixy stuff and Haskell

The GHC Haskell compiler and library set it comes with has a nice bit of POSIX functionality. Today, however, I found myself really wanting for a program that could popen a program, giving me back a pair of handles to communicate with the external program, and allow me to do Expect-like things with the interaction.

Essentially I need a bot. The bot needs to talk to a device that speaks SMASH CLP, and my initial thoughts were to write some Haskell code around the useful function interact, or rather, a close relative of interact.

Interact has the signature

interact :: (String -> String) -> IO ()

This just means that it takes a function which takes a String as input and produces a String as output, and the result of interact is an IO action that returns ().

What this gives me is the ability to process input strings, to produce output strings, which get read in and written out by the IO action that is the result of interact. It interleaves the pure functional processing around the input to create output.

The way it works is by calling the function getContents, which lazily reads all the input from stdin and processes it through the provided (String -> String) function, to produce output.

At first glance, this is not seemingly a very advantageous function, except when you take into account all the ways you can set up the IO before calling interact to get some nice behaviors.

For example, if one sets the handles for stdin and stdout to "LineBuffer" based buffers before calling interact, you can get linewise input by doing the following inside the (String -> String) processing function.

lineId = unlines . lines

The function lineId breaks up the lazy input string into lines, then re-assembles them by composing the function lines and unlines together.

The type of the function lines is:

lines :: String -> [String]

The type of the function unlines is

unlines :: [String] -> String

One would think that these two functions would just negate each other, and they do when combined as "unlines . lines" returning the original input to lines, however, if I add further behaviors to the function composition expression that operate on the [String] result of lines before running unlines I have the opportunity to work on each member of the list.

This gives me guaranteed linewise input evaluation of commands, and the ability to interpret line-oriented protocols, strings or what have you.

Now think of how useful interact can be?

Now, interact by itself is only dealing with stdin and stdout. Perhaps I want to open a connection to a remote service, or even a stream of in and out data. It is not terribly hard to write a relative to interact that can handle those sources of data.

hInteract :: Handle -> Handle -> (String -> String) -> IO ()
hInteract inp out f = hPutStr out . f =<< hGetContents inp

This function can take a handle for input, a handle for output, and a function of String -> String, and uses them to lazilly read input from the input handle, pushing data through the String -> String function, to finally output the result string to the output handle.

All of the interesting parts of this function are still contained in f.

The part that's kind of exciting now is that I don't have to limit myself to just line based input if I consider using functions like words and unwords.

I think I'm nearly ready to write an almost IO free, pure version of my Expect-like code bot in Haskell.

1 comment:

  1. hInteract should say:

    hInteract inp out f = hPutStr out . f =<< hGetContents inp

    unfortunately I didn't realize blogspot had bugs when it came to certain character strings.

    ReplyDelete