A lighter-weight alternative to runhaskell for quick scripts

I labelled this as uncategorized as it’s somewhere between a question and a show-and-tell.

I was helping someone use knitr to embed and run Haskell code in Quarto.
The knitr code looks like this:

knitr::knit_engines$set(haskell = function(options) {
code <- options$code
codefile <- tempfile(fileext = ".hs")
on.exit(file.remove(codefile))
out <- system2(
file.path(path.expand('~'), '.ghcup/bin/ghc'),
c('-package text', '-e',"':script ", codefile, "'"),
stdout = TRUE
)
knitr::engine_output(options, code, out)
})

What this does is:

  • Read a snippet of text into a temp file
  • Run it with ghc -package text -e “:script temp.hs”

So now you can run snippets like:

a = [2, 9, 6]

:{
updateSecond (x:_:z) y = x : y : z
updateSecond xs _ = xs
:}

updateSecond a 4

But you still need to put the braces around multiline strings or any I/O which makes it look a little ugly. We decided to create a script that parses the temp file and inserts the braces so you can instead just write.

a = [2, 9, 6]

updateSecond (x:_:z) y = x : y : z
updateSecond xs _ = xs

updateSecond a 4

This pattern looks like it’s generally useful for ghci-type scripts. In fact it might be a cool way to run quick scripts without a main function. So we threw together a program (originally in Haskell but used a LLM to translate it to AWK cause the process dependency makes it a little less convenient) to do the brace insertions.

Now we can do things like run a script without a main for quick prototyping etc.

x = [1..10]
y = [5..15]

import Data.List

z = sort (x ++ y)

print z

map (+2) z

import Data.Function

y' = z & drop 5 & reverse & drop 5 & reverse

print y'

doubleEveryOther :: [Int] -> [Int]
doubleEveryOther xs
  | null xs   = ys
  | otherwise = zipWith ($) (cycle [id, (*2)]) xs
    where
      ys = [2,2,2]

doubleEveryOther []

So now you can do hscript test.hs and this runs fine. Is there a way to get the same behaviour (run an interpreted, potentially multiline script) without all this ceremony? If not it would be a very convenient feature to add since runhaskell still expects a lot more structure than a really quick script.

For some literate programming this ends up being pretty useful e.g. if you already had some file that imported all the relevant files and set all the right extensions you could make the literate programming look a lot prettier:

:script setup.ghci

normalize :: DataFrame -> DataFrame
normalize = ...

df <- D.readCsv "./data/test.csv"
print (D.describeColumns (normalize df))
12 Likes

I wonder if this could be formulated as a preprocessor, than you could specify runhaskell -pgmL descrambler test.hs. Not quite zero ceremony, but potentially more widely applicable.

3 Likes

That would be a great idea. Where does one typically file these feature requests?

a feature request on the ghc tracker is the place to put it, but as you can see there’s lots to be done and a narrow pool of developers, so it may well languish if someone doesn’t get excited by it and pick it up (or if you don’t jump in yourself) Issues · Glasgow Haskell Compiler / GHC · GitLab

This may not be so much a new feature of GHC, or any other existing tool I know of. The option -pgmL of GHC simply takes an executable path and runs the source code through it. If you can write down a specification of how exactly the executable should transform its input, it can be implemented, perhaps as a parser → transformer → pretty-printer pipeline.

1 Like

markdown-unlit is a pre-processor, for example. No GHC hacking needed!

2 Likes