12 December 2020

Functions as first-class citizens: the shell-ish version

functional-bash

The idea to compose multiple functions together, passing one or more of them to another as parameters, generally referred to as using higher order functions is a pattern which I’m very comfortable with, since I read about ten years ago the very enlighting book Functional Thinking: Paradigm Over Syntax by Neal Ford. The main idea behind this book is that you can adopt a functional mindset programming in any language, wheter it supports function as first-class citizens or not. The examples in that book are mostly written in Java (version 5 o 6), a language that supports (something similar to) functions as first-class citizens only from version 8. As I said, it’s more a matter of mindset than anything else.

So: a few days ago, during a lab of Operating System course, waiting for the solutions written by the students I was wondering If it is possible to take a functional approach composing functions (or something similar…) in a (bash) shell script.

(More in detail: the problem triggering my thinking about this topic was "how to reuse a (not so much) complicated piece of code involving searching files and iterating over them in two different use cases, that differed only in the action applied to each file)

My answer was Probably yes!, so I tried to write some code and ended up with the solution above.

The main point is - imho - that as in a language supporting functions as first class citizens the bricks to be put together are functions, in (bash) script the minimal bricks are commands: generally speaking, a command can be a binary, or a script - but functions defined in (bash) scripts can be used as commands, too. After making this mental switch, it’s not particularly difficult to find a (simple) solution:

action0.sh - An action to be applied to each element of a list

#!/bin/bash
echo "0 Processing $1"

action1.sh - This first action to be applied to each element of a list

#!/bin/bash
echo "1 Processing $1"

foreach.sh - Something similar to List<T>.ForEach(Action<T>) extension method of .Net standard library(it’s actually a high order program)

#!/bin/bash
action=$1
shift
for x
do
    $action $x
done

main.sh - The main program, reusing foreach’s logic in more cases, passing to the high order program different actions

#!/bin/bash
./foreach.sh ./action0.sh $(seq 1 6)
./foreach.sh ./action1.sh $(seq 1 6)

./foreach.sh ./action0.sh {A,B,C,D,E}19
./foreach.sh ./action1.sh {A,B,C,D,E}19

Following this approach, you can apply different actions to a bunch of files, without duplicating the code that finds them… and you do so applying a functional mindset to bash scripting!

In the same way it is possible to implement something like the classic map higher order function using functions in a bash script:

double () {
    expr $1 '*' 2
}

square () {
    expr $1 '*' $1
}

map () {
    f=$1
    shift
    for x
    do
        echo $($f $x)
    done
}

input=$(seq 1 6)
double_output=$(map "double" $input)
echo "double_output --> $double_output"
square_output=$(map "square" $input)
echo "square_output --> $square_output"
square_after_double_output=$(map "square" $(map "double" $input))
echo "square_after_double_output --> $square_after_double_output"

square_after_double_output, as expected, contains values 4, 16, 36, 64, 100, 144.

In conclusion… no matter what language you are using: using it functionally, composing bricks and higher order bricks together, it’s just a matter of mindset!

Written with StackEdit.

No comments:

Post a Comment