DEV Community

kohei_wada
kohei_wada

Posted on Originally published at wada-dev.com

Beyond AWK: Bringing Haskell into Unix Pipelines

1. A Problem Born from Daily Developer Life

"I want to use Haskell more in real work."

For any developer who has tasted the beauty of functional programming, this is a heartfelt wish. But the reality is that Haskell gets shunned as "impractical" or "too expensive to introduce," and there are almost no opportunities to use it at work.

Couldn't we at least use Haskell for the small everyday tasks? When you stop and look, a shell pipeline really does resemble function composition.

cat file | grep pattern | sort | uniq
Enter fullscreen mode Exit fullscreen mode

Pipeline = function composition. The more I sat with that intuition, the more convinced I became that Haskell ought to fit right in.

The AWK Wall

When you want to do something a bit longer than a typical one-liner, AWK comes to mind. But...

  • "How do you write an array in AWK again?"
  • "What's the syntax for a for loop...?"

Haven't you ended up wasting time on Google, watching all the fun of shell trickery slip away?

2. The Moment of Inspiration: The Birth of phs

My Three Big Problems

  1. No chance to use Haskell at work
  2. Wanting more powerful processing inside shell pipelines
  3. Hating having to look up AWK syntax every time

I tried existing options like stack script and cabal script, but the first run is way too slow. Waiting several seconds for a trivial bit of processing just isn't practical.

"Haskell ought to be more usable than this"

Convinced of that, I figured fine, I'll just build it myself. If we use ghc -e for immediate execution, couldn't we drop Haskell directly into a pipeline?
That is phs (PipeLine Haskell Script).

3. phs in Everyday Work

Example 0: stack script is too slow

#!/usr/bin/env stack
-- stack script --resolver lts-22.33
main = interact (show . length . lines)
Enter fullscreen mode Exit fullscreen mode

This code takes several seconds on its first run. So you end up reaching for wc -l instead, and Haskell drifts further out of daily life.

With phs, it's instant:

cat file.txt | ./phs --all 'length'
Enter fullscreen mode Exit fullscreen mode

Example 1: Character count per line

In AWK:

awk '{print length($0)}' file.txt
Enter fullscreen mode Exit fullscreen mode

But with phs, it's intuitive:

cat files.txt | ./phs 'length'
Enter fullscreen mode Exit fullscreen mode

Example 2: Word count and character count

# AWK
awk '{print NF, length($0)}' file.txt

# phs
./phs '\s -> (length (words s), length s)'
Enter fullscreen mode Exit fullscreen mode

Example 3: Sum of numbers

# AWK
awk '{sum=0; for(i=1;i<=NF;i++) sum+=$i; print sum}'

# phs
./phs 'sum . map read . words'
Enter fullscreen mode Exit fullscreen mode

4. A Thorough Comparison of AWK and Haskell (phs)

The classic text-processing tool of UNIX culture is AWK. But phs covers what AWK is good at while pushing expressiveness even further.

Item AWK phs (Haskell)
First-run speed Instant Instant (via ghc -e)
Syntax Custom syntax (C-like) Standard Haskell
Functions / types Built-ins are limited Haskell's standard library is available
Numeric processing Floating point by default Precision managed via types
Extensibility Relies on external commands Extensible by adding modules
Learning cost Must memorize a custom grammar Zero if you already know Haskell

Comparison on basic text processing

Count characters per line

# AWK
awk '{print length($0)}'

# phs
./phs 'length'
Enter fullscreen mode Exit fullscreen mode

Take the first 10 characters of each line

# AWK
awk '{print substr($0, 1, 10)}'

# phs
./phs 'take 10'
Enter fullscreen mode Exit fullscreen mode

Filter lines that are uppercase only

# AWK (regex)
awk '/^[A-Z]+$/'

# phs (functional)
./phs 'all isUpper'
Enter fullscreen mode Exit fullscreen mode

Comparison on numeric processing

Sum of numbers per line

# AWK (imperative loop)
awk '{sum=0; for(i=1;i<=NF;i++) sum+=$i; print sum}'

# phs (function composition)
./phs 'sum . map read . words'
Enter fullscreen mode Exit fullscreen mode

Sum of numbers across all lines

# AWK (messy state handling)
awk '{for(i=1;i<=NF;i++) total+=$i} END {print total}'

# phs (simple function composition)
./phs --all 'sum . map read'
Enter fullscreen mode Exit fullscreen mode

Example: word count per line

AWK:

awk '{print NF}' file.txt
Enter fullscreen mode Exit fullscreen mode

phs:

./phs 'length . words'
Enter fullscreen mode Exit fullscreen mode

Where the gap really shows: mathematical algorithms

First 10 terms of the Fibonacci sequence

AWK (a tangle of iteration):

awk 'BEGIN{a=1;b=1;for(i=1;i<=10;i++){print a;c=a+b;a=b;b=c}}'
Enter fullscreen mode Exit fullscreen mode

phs (an elegant recursive definition):

seq 1 10 | ./phs 'let fib n = if n <= 2 then 1 else fib (n-1) + fib (n-2) in fib . read'
Enter fullscreen mode Exit fullscreen mode

Collatz conjecture

AWK (verbose imperative code):

awk '{n=$1; while(n>1){if(n%2==0)n=n/2;else n=3*n+1; print n}}'
Enter fullscreen mode Exit fullscreen mode

phs (the formula expressed as-is):

seq 10 | ./phs 'let collatz n = if n == 1 then [1] else n : collatz (if even n then n `div` 2 else 3*n+1) in collatz . read'
Enter fullscreen mode Exit fullscreen mode

Power set

AWK: I don't want to (you'd need multi-dimensional arrays)

phs: You can write the mathematical definition verbatim

echo "abc" | ./phs 'let powerset [] = [[]]; powerset (x:xs) = powerset xs ++ map (x:) (powerset xs) in powerset'
Enter fullscreen mode Exit fullscreen mode

Advanced text processing

Sorting lines

# AWK (depends on an external command)
awk '{print}' | sort

# phs (self-contained via built-in functions)
./phs --all 'sort'
Enter fullscreen mode Exit fullscreen mode

Removing duplicate lines

# AWK (needs external commands)
awk '{print}' | sort | uniq

# phs (solved in one shot)
./phs --all 'nub'
Enter fullscreen mode Exit fullscreen mode

Reversing line order

# AWK (requires a clunky implementation)
awk '{a[NR]=$0} END {for(i=NR;i>0;i--) print a[i]}'

# phs (intuitive)
./phs --all 'reverse'
Enter fullscreen mode Exit fullscreen mode

Joining all lines into a single string

# AWK (newline handling is a pain)
awk '{printf "%s%s", (NR>1?" ":""), $0} END {print ""}'

# phs (natural)
./phs --all 'unwords'
Enter fullscreen mode Exit fullscreen mode

Conclusion:

  • AWK is ideal for "simple row/column processing"
  • phs shines at "mathematical or recursive processing," "type-safe aggregation," and "functional transformations," all writable as one-liners
  • Especially for complex algorithms, AWK turns into verbose imperative code, while phs can express the mathematical definition directly

5. The Edge in Mathematical Algorithms

Collatz conjecture

AWK: imperative and verbose

awk '{n=$1; while(n>1){if(n%2==0)n=n/2;else n=3*n+1; print n}}'
Enter fullscreen mode Exit fullscreen mode

phs: the formula expressed directly

seq 10 | ./phs 'let collatz n = if n == 1 then [1] else n : collatz (if even n then n `div` 2 else 3*n+1) in collatz . read'
Enter fullscreen mode Exit fullscreen mode

Power set

AWK: I don't want to

phs: write the mathematical definition verbatim

echo "abc" | ./phs 'let powerset [] = [[]]; powerset (x:xs) = powerset xs ++ map (x:) (powerset xs) in powerset'
Enter fullscreen mode Exit fullscreen mode

6. The Simplicity of phs

The implementation of phs is shockingly simple:

  • Command-line parsing
  • Immediate evaluation via ghc -e
  • Config file loading

That's all you need for a lightweight implementation that works. The secret to its speed is ghc -e's immediate execution with no precompilation required.

Current limitations and future improvements

Because phs is a lightweight implementation, it has a few constraints:

String display issue:

echo "hello" | ./phs 'id'
# Output: "hello"  (double quotes are included)
Enter fullscreen mode Exit fullscreen mode

This happens because results are displayed using the Show instance.

But anyone who regularly uses the shell can just do:

echo "hello" | ./phs 'id' | tr -d '"'
# Output: hello  (double quotes removed)
Enter fullscreen mode Exit fullscreen mode

Problem solved (lol).

The current simple implementation stays lightweight by running every result through show uniformly. It's a design that follows the Unix philosophy of "rather than chasing perfection, get something working first." Any fine-grained tweaking is best handled by combining it with other shell tools — that's the Unix way!

7. How phs Sets Itself Apart from Existing Tools

Approach First run Subsequent runs Learning cost
stack script 3-5 sec Instant Medium
cabal script 2-4 sec Instant Medium
AWK Instant Instant High (you forget)
phs Instant Instant Low

Learning and research

  • Algorithm exploration
  • Generating and analyzing number sequences
# Sum of squares
seq 1 100 | ./phs --all 'sum . map (\x -> let t = read x in t * t)'

# Searching for perfect numbers
seq 1 1000 | ./phs --all 'let isPerfect n = sum [i | i <- [1..n-1], n `mod` i == 0] == n in \xs -> filter isPerfect $ map read xs'
Enter fullscreen mode Exit fullscreen mode

8. Looking Ahead

phs has made "everyday Haskell" a reality:

  • No need to recall AWK syntax
  • The beauty of functional programming, every day

9. Getting Started with phs Today

git clone https://github.com/Kohei-Wada/phs.git
cd phs
chmod +x phs

echo "hello world" | ./phs 'reverse'
Enter fullscreen mode Exit fullscreen mode

And you'll notice: "oh, this is actually handy." (lol)

Repository: https://github.com/Kohei-Wada/phs


Let's bring Haskell more into daily life!

phs is more than just a tool. It's a first step toward folding Haskell into everyday work. Why not join me in bringing the beauty of functional programming into your daily tasks?

I'm pretty sure it'll make writing code a lot more fun.


This post was written with AI assistance and reviewed by the author.

Top comments (0)