The concept of "imperative shell function core" is a specialized application of the Functional Core, Imperative Shell (FCIS) architectural pattern. It structures software by isolating the business logic and computations (the "functional core") from interactions with the outside world and state changes (the "imperative shell"). When applied to shell functions, this design produces robust, testable scripts by clearly separating pure logic from side effects like file I/O or network calls.
The functional core: Pure logic and calculations
At the heart of the pattern is the "functional core," which is composed of pure shell functions. These functions have the following characteristics:
- No side effects: They do not interact with the filesystem, write to the console, or change the state of the system outside of their scope.
- Consistent output: For any given input, a pure function will always produce the same output. Their behavior is predictable and deterministic.
- Simple data transformation: They take data as input, perform a computation or transformation, and return a result. Their purpose is limited to pure calculation.
- Easily testable: Because they only depend on their input and produce no side effects, these functions can be tested in isolation with simple unit tests.
A pure shell function might, for example, process a string or calculate a value based on its arguments. It would not perform any echo or file operations directly.
The imperative shell: Handling side effects
Surrounding the functional core is the "imperative shell," which handles all the "messy" parts of the real world. This part of the code is responsible for:
- I/O operations: Reading from files, accepting user input from
stdin, or interacting with databases. - Orchestration: Calling functions from the functional core in a specific order and managing the flow of data.
- Side effects: All actions that mutate state, such as writing to a file, printing to
stdoutwithecho, or sending data over a network, are contained within the shell. - Interaction with the OS: The shell is the layer that interfaces with the operating system, network, and other external services.
The imperative shell is deliberately kept as simple as possible. It fetches input, passes it to the functional core for computation, and then takes the core's output to produce the necessary side effects.
The interplay of core and shell
The relationship between the functional core and imperative shell defines how a script operates:
- Input: The imperative shell reads input from an external source (e.g., a file,
stdin, or command-line arguments). - Conversion: The shell may reformat or adapt the input so that it can be processed by the functional core.
- Calculation: The shell passes the formatted data to a pure function in the functional core.
- Transformation: The functional core performs its deterministic calculation and returns a result to the imperative shell.
- Output: The imperative shell receives the result and applies the necessary side effect, such as printing to the console or writing to a file.
Example: A price calculation script
Consider a script that calculates the final price of an order with a discount.
**Functional Core (pure logic):**The business logic is contained in pure functions that only perform calculations.
# functional_core.sh
# calculate_discount() is a pure function.
# It takes the total and discount percentage as arguments and returns the new total.
# No I/O or side effects.
calculate_discount() {
local total=$1
local discount_percent=$2
local discount_amount=$(echo "scale=2; $total * ($discount_percent / 100)" | bc)
local final_total=$(echo "scale=2; $total - $discount_amount" | bc)
echo "$final_total"
}
Use code with caution.
**Imperative Shell (side effects and orchestration):**The main script is the shell, handling user interaction and calling the pure core function.
#!/bin/bash
# imperative_shell.sh
# Include the functional core
source functional_core.sh
# 1. Action: Get input from the user (side effect)
echo "Enter the total purchase amount:"
read total_amount
echo "Enter the discount percentage (e.g., 15 for 15%):"
read discount_rate
# 2. Calculation: Pass data to the functional core
final_price=$(calculate_discount "$total_amount" "$discount_rate")
# 3. Action: Produce output (side effect)
echo "The final price after a $discount_rate% discount is: $$final_price"
Use code with caution.
Benefits of the FCIS pattern in shell scripting
- Improved testability: The pure functions in the
functional_core.shscript can be easily unit-tested by providing inputs and checking the output without worrying about external state. - Greater reliability: Isolating business logic from side effects reduces the potential for unexpected behavior, making the script more predictable.
- Increased maintainability: Complex business logic is cleanly separated from the script's infrastructure, making it easier to read and modify specific parts without introducing bugs elsewhere.
- Enhanced reusability: The pure functions in the functional core can be reused in different scripts or modules, as they are decoupled from the specific I/O of any single application.
- Clearer separation of concerns: The pattern clearly distinguishes what the script does (the core) from how it interacts with the world (the shell).