Simple Haskell scripting


It's been quite some time since Martín Escardó told me about the somehow forgotten Haskell function

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

What it does is that it takes the function String -> String and simply throws the entire program input into it and whatever it outputs produces as the program output. For example, the following Haskell program prints back the first 10 characters of its input.

main :: IO ()
main = interact (take 10)

This becomes really useful when chained with the lines :: String -> [String] and unlines :: [String] -> String functions. Then writing Haskell scripts that deal with text data, with entries split by lines, is just simple. The usual Haskell script then looks something like this.

main :: IO ()
main = interact pipe
pipe = unlines . map linepipe . lines

linepipe :: String -> String
linepipe = ... -- a function that handles a single line of input

There are quite a few Haskell scripting libraries out there and they get quite a bit of attention. However, I haven't seen many articles praising the simplicity and power of the interact+lines+unlines pattern.

As a bonus, here is one real-world example. Let's say we want to convert the CSV data of this form

Alice;travelling, maths;https://alice.crypto

into an html of this form

<li><a href="https://alice.crypto">Alice</a> (travelling, maths)</li>
<li>Bob (espionage)</li>

The following is an easy script I wrote to do just that with interact. I decided to use the Data.Text.Lazy version of interact because I needed to deal with unicode characters properly. The benefit of the lazy version is that the script can handle inputs that don't fit into the memory.

#!/usr/bin/env runhaskell

{-# LANGUAGE OverloadedStrings #-}

import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.IO as T

main :: IO ()
main = do
putStrLn "<ul>"
T.interact pipe
putStrLn "</ul>"
pipe :: T.Text -> T.Text
pipe = T.unlines . map linepipe . T.lines

linepipe :: T.Text -> T.Text
linepipe line =
"<li>" <> name <> hobbies <> "</li>"
(a:b:c:xs) = T.split (== ';') line

name | c == "" = a
| otherwise = "<a href=\"" <> c <> "\">" <> a <> "</a>"

hobbies | b /= "" = " (" <> b <> ")"
| otherwise = ""

To run it, simply type cat data.csv | ./ This should work provided that is executable and the package text is installed.

Responses (?)

Indieweb interactions: Like/Reshare/Reply/Bookmark with Quill or Like/Reshare/Reply/Bookmark with Micropublish.