Open Source im professionellen Einsatz
Linux-Magazin Online Artikel/
(c) Christopher Meder, 123RF

(c) Christopher Meder, 123RF

Shell scripting with type-safety using Haskell

Strong Types


Why is scripting usually done in dynamically typed languages? This article applies strong typing in Haskell to shell programs. The end result can still be light-weight but also save time by reducing runtime errors.


We can use a DSL in Haskell (a strongly-typed functional programming language) for shell-scripting that looks very similar to bash.

main = shelly $ do
    apt_get "update" []
    apt_get "install" ["haskell-platform"]
    apt_get mode more = run "apt-get" (["-y", "-q", mode] ++ more)

Our goal however is not imitation, but a better environment for systems programming. The main benefit we now have is type-safety: we can let the type system catch errors at compile time. But these are not ordinary C/Java types: you don't see any type signatures here because Haskell has type-inference, which lets us sparsely specify types and have the compiler figure out the rest. The type system is also more powerful: eventually we will see how it allows us to express more invariants at compile-time.

Reducing Errors

I am motivated to reduce errors in scripts because the cost of testing and failure can be higher than other programs. Automated testing of scripting is inherently more difficult because we are testing interaction with other programs. At the same time, the cost of not testing can be higher because we may leave our system in a state that requires manual work to be undone, which in the case of deployment scripts could include a non-functioning or even inaccessible state.

For our purposes we can define a script as a program focused on interacting with the OS or other programs. There must be a reason why this has traditionally been left to dynamic languages. I suspect it is largely because scripting languages such as Perl, Ruby, and Bash are high-level languages that have simple interfaces for OS interaction. Lets keep high-level but instead start with strong typing and try to figure out how to make interaction with the OS as convenient as possible. The standard Haskell libraries exposes a complete set of primitives, but lacks an easy to use and flexible API. We can use the language's flexibility to create a nicer interface.

The Haskell Platform makes installing the compiler and the libraries easy.

I would encourage you to follow this code more closely by first installing Haskell and then compiling snippets with 'ghc'. You will need to first install Shelly with the command: 'cabal install shelly shelly-extra' Try this one out:

{-# LANGUAGE OverloadedStrings #-}
import Shelly

main = shelly $ do
    appendfile "a.hs" "main = print \"hello\""
    run "ghc" ["a.hs"]
    out <- run "./a" []
    echo out

You can save it in a file 'makea.hs', and then run 'ghc makea.hs && ./makea.hs'
Note that the top 2 lines are also required to use Shelly in future examples.
If we put a number in the second argument of appendfile, or omit the argument completely, we get a type error. A new user might struggle to understand some of the error messages, but the important thing is we know at compile time the exact line where there is an error.

Almost like a shell script: Haskell compiles source code and exectues the result.

To help follow along with basic Haskell code it might help to take a look at the "Learn You a Haskell" book. We are going to see some type signatures now. You can learn more about them from the book.

Pure Computation


In addition to adding type-safety, Haskell is also arguably the most radical departure from the traditional scripting paradigm we can choose. Scripting is focused on interacting with the OS, but in Haskell they type system separates this from pure computation.

add_two :: Int -> Int
add_two x = x + 2

This is a function definition. In javascript, this function would be written as: 'function add_two(x) { return x + 2; }'

The function 'add_two' by definition cannot interact with the OS. It also cannot mutate a variable such as x in any way. Haskell variable are actually constants! Purity pays off greatly for the compiler, which knows it can optimize the code. This is great for parallelism and concurrency, because pure code is thread-safe. Purity also makes it easier to understand what is going on: just by the type signature we know what this function is *not* capable of doing: scripting!

import IO

add_2_input :: IO Int
add_2_input = do
   line <- getLine
   return (add_two (read line))

add_two :: Int -> Int
add_two x = x + 2

Here we define 'add_2_input' explicitly as a function that will perform IO, or input/output.
'read' converts a String to a Haskell value. It knows to convert to an 'Int' because of type inference from the 'Int' in the type signature. The important thing to note is that the type signature 'IO Int' tells us there is input/output, or scripting that returns an Int. You can run this code as an interactive program:

 main = add_2_input >>= print

After compiling with ghc, one can run the program. Entering the number 5 gives:


'IO' is a monad. Operations in a monad can use the 'do' notation syntax seen above.
For our purposes, a monad defines an execution environment. The goal of the Shelly library is to define an optimal environment for scripting, it is the monad ShIO.

Linux-Magazin kaufen

Einzelne Ausgabe
Bald erhältlich
Get it on Google Play


Ähnliche Artikel

  • Haskell

    Haskell ist keine Skriptsprache, sondern wird in der Regel kompiliert. Mit einigen Handgriffen lassen sich aber Shellskripte in die funktionale Sprache einbinden. Haskells starkes Typensystem greift dann ein, um Fehler durch Argumente in falscher Anzahl oder Art zu verhindern.

comments powered by Disqus


Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.