Skip to content
Published on

The Practical Value of Functional Programming — Monad, Functor, Pure Functions, Haskell, Erlang/Elixir, Gleam, Effect-TS, Phoenix LiveView (2025)

Authors

TL;DR — Functional programming (FP) is no longer a mathematician's hobby; it's the mindset of the concurrency and distribution era. React Hooks is functional state, Redux is reduce, RxJS is the Observable Monad, Swift/Rust/Kotlin's Option/Result are Maybe/Either Monads, and Erlang/Elixir achieves 99.9999999% (nine nines) availability via pure functions plus the actor model. In 2024-2025, a new generation emerged: Effect-TS (TypeScript), Gleam (typed language on BEAM), Elixir + Phoenix LiveView, Roc (Elm's successor), and Unison (distributed pure). This piece traces why "side-effect management" — FP's core — matters more in the cloud era, how to understand Monads without category theory, and what modern languages borrowed from FP.

Why Functional, Now

FP began with LISP (1958), flowered through Haskell and ML in the 1980s-90s, but industry called it "the academics' language." That changed mid-2010s:

  • 2015: React declared immutable props + pure render function
  • 2016: Redux — state = reducer(state, action), effectively reduce
  • 2017: React Hooks proposed (shipped 2019) — functions over classes
  • 2019: Rust 1.0 adopts ownership + Result/Option wholesale
  • 2020: Swift 5.5 — async/await + Task + Result
  • 2021: Kotlin Result<T>, Java Optional<T> mainstream
  • 2023: TypeScript Effect-TS 1.0 — "the Haskell of TypeScript"
  • 2024: Gleam 1.0 — typed FP on BEAM (Erlang VM)
  • 2024: Zed Editor, Fly.io Phoenix LiveView — Elixir revival
  • 2025: WhatsApp (2B users), Discord, Pinterest, Bleacher Report expand Erlang/Elixir

Three reasons why now:

  1. Concurrency — tens of thousands of concurrent requests + shared-state lock hell → immutability and message passing as the answer
  2. Reliability — partial failure in distributed systems → "let it crash" vindicated
  3. Type safety — algebraic data types + pattern matching demonstrably cut bugs

Pure Functions — the starting point

A pure function: same input, same output, no side effects.

// pure
function add(a, b) { return a + b }
function double(arr) { return arr.map(x => x * 2) }

// impure
let counter = 0
function incCounter() { counter++; return counter }

function logAndAdd(a, b) {
  console.log(a, b)
  return a + b
}

function fetchUser(id) {
  return fetch(`/api/users/${id}`)
}

Practical Value

  1. Testable — input alone reproduces output; no mocks
  2. Parallelizable — no shared state, thread-safe by default
  3. Memoizable — caching by input (React.memo, useMemo)
  4. Reasonable — readers trust there's no hidden global mutation
  5. Composable — predictable combination

Referential transparency: replacing a call with its result preserves program meaning. Another name for purity.

Side Effects Never Vanish

Real programs need I/O, DB, and network. FP's insight: don't eliminate side effects — push them to the edges and encode them in types.

  • Haskell: IO a
  • Rust: Result<T, E>
  • Effect-TS: Effect<R, E, A> — requirement, error, value
  • Elm: Cmd/Sub — runtime handles effects

Immutability — Forbidding Mutation by Design

// mutable
const arr = [1, 2, 3]
arr.push(4)
const obj = { a: 1 }
obj.a = 2

// immutable
const arr2 = [1, 2, 3]
const arr3 = [...arr2, 4]
const obj2 = { a: 1 }
const obj3 = { ...obj2, a: 2 }

Why Immutability Matters

  1. Change tracking — React's prev === next comparison in O(1)
  2. Concurrency-safe — multiple readers, no problem
  3. Time-travel debugging — why Redux DevTools works
  4. Easy undo/redo — keep past states
  5. Easy memoization — same reference = same value

Persistent Data Structures — the performance fix

Naive immutability copies everything (O(n)). Persistent data structures use structural sharing for O(log n).

  • Immutable.js — List, Map, Set
  • Immer — write to a draft, get an immutable copy
  • Clojure — built-in Hash Array Mapped Trie
  • Scala — persistent Vector, HashMap
import { produce } from 'immer'

const state = { users: [{ id: 1, name: 'Alice' }] }
const nextState = produce(state, draft => {
  draft.users.push({ id: 2, name: 'Bob' })
  draft.users[0].name = 'Alice Updated'
})

Higher-Order Functions — Functions as Values

[1, 2, 3].map(x => x * 2)
[1, 2, 3].filter(x => x > 1)
[1, 2, 3].reduce((acc, x) => acc + x, 0)

const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x)
const toString = x => x.toString()
const exclaim = s => s + '!'
const shout = s => s.toUpperCase()

const greet = compose(shout, exclaim, toString)
greet(42)  // '42!'

const curry = (fn) => {
  return function curried(...args) {
    if (args.length >= fn.length) return fn(...args)
    return (...more) => curried(...args, ...more)
  }
}

const add = curry((a, b, c) => a + b + c)
add(1)(2)(3)

Pipelines — Left to Right

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x)

const result = pipe(
  filter(u => u.active),
  map(u => u.email),
  xs => xs.join(', ')
)(users)

Elixir has the pipe operator natively:

users
|> Enum.filter(& &1.active)
|> Enum.map(& &1.email)
|> Enum.join(", ")

Option / Maybe — A World Without null

Tony Hoare called null the "billion-dollar mistake." FP languages encode absence in the type system.

fn find_user(id: u64) -> Option<User> {
    if id == 0 { None } else { Some(User { id, name: "Alice".into() }) }
}

find_user(42)
    .map(|u| u.name)
    .unwrap_or("anonymous".into())
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'

const findUser = (id: number): O.Option<User> =>
  id === 0 ? O.none : O.some({ id, name: 'Alice' })

const result = pipe(
  findUser(42),
  O.map(u => u.name),
  O.getOrElse(() => 'anonymous')
)
findUser :: Int -> Maybe User
findUser 0 = Nothing
findUser id = Just User { userId = id, userName = "Alice" }

Result / Either — Typed Failure

Exceptions hide control flow from signatures. Result/Either encodes success/failure in types.

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 { Err("division by zero".into()) }
    else { Ok(a / b) }
}

let result = divide(10.0, 2.0)?;
import { Effect } from 'effect'

const divide = (a: number, b: number): Effect.Effect<never, string, number> =>
  b === 0
    ? Effect.fail("division by zero")
    : Effect.succeed(a / b)

const program = Effect.gen(function* () {
  const x = yield* divide(10, 2)
  const y = yield* divide(x, 0)
  return x + y
})

Monad — What Even Is It

A Monad is FP's notorious concept. It comes from category theory, but practically it's just a chainable wrapping type.

Definition (practical)

A Monad provides two operations:

  1. of(x) — wrap x into the Monad (unit, pure, return)
  2. flatMap(f) — apply f to the inner value; f returns a Monad (bind)
type Maybe<T> = { tag: 'some', value: T } | { tag: 'none' }

const of = <T>(x: T): Maybe<T> => ({ tag: 'some', value: x })

const flatMap = <A, B>(m: Maybe<A>, f: (a: A) => Maybe<B>): Maybe<B> =>
  m.tag === 'some' ? f(m.value) : m

const parseInt = (s: string): Maybe<number> => {
  const n = Number(s)
  return isNaN(n) ? { tag: 'none' } : { tag: 'some', value: n }
}

const addTen = (n: number): Maybe<number> => of(n + 10)

const result = flatMap(parseInt("42"), addTen)

Why Monads

Monads let diverse side effects share one interface:

  • Maybe/Option — absence
  • Either/Result — failure
  • Promise/Future/IO — async/I-O
  • List — nondeterminism
  • State — state mutation
  • Reader — environment
  • Writer — logging

Monad Laws

  1. Left identity: of(x).flatMap(f) === f(x)
  2. Right identity: m.flatMap(of) === m
  3. Associativity: m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))

do-notation

Haskell:

main = do
  line <- getLine
  let n = read line :: Int
  print (n * 2)

TypeScript (Effect-TS):

const program = Effect.gen(function* () {
  const user = yield* fetchUser(1)
  const posts = yield* fetchPosts(user.id)
  return { user, posts }
})

JavaScript async/await is essentially the Promise Monad's do-notation.

Functor, Applicative, Monad — Three Layers

The FP type-class hierarchy:

  • Functor — only map. "Apply a function inside a container."
  • Applicative — Functor + of + ap. "Combine multiple wrapped values." Used for collecting validation errors. The combinators <$> and <*> in Haskell are Applicative.
  • Monad — Applicative + flatMap. "Sequence that depends on the previous result."

Haskell — Lessons From "the Mathematicians' Language"

Haskell (1990) is pure, lazy, statically typed. Niche in production, but its influence on modern languages is enormous.

Haskell's Innovations

  • Type class — ad-hoc polymorphism (root of Rust traits, Scala implicits, Swift protocols)
  • Algebraic Data Types — sum + product (Rust enum, Swift enum, TS discriminated union)
  • Pattern matching — universal now
  • Monad — encode I/O as pure functions
  • Lazy evaluation — infinite list [1..]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
take 10 fibs  -- [0,1,1,2,3,5,8,13,21,34]

class Eq a where
  (==) :: a -> a -> Bool

data Shape = Circle Double | Rectangle Double Double | Triangle Double Double Double

area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
area (Triangle a b c) = sqrt (s * (s-a) * (s-b) * (s-c))
  where s = (a + b + c) / 2

Haskell in Production

  • Facebook Sigma — spam filtering (10B requests/day)
  • GitHub Semantic — code analysis
  • Standard Chartered — financial modeling
  • Target — price optimization
  • Mercury — banking (since 2020)

Erlang/Elixir — Nine-Nines Availability

Erlang was built by Ericsson in 1986 for telephone switches. Elixir (José Valim, 2012) layers Ruby-like syntax atop the Erlang VM (BEAM).

BEAM's Four Superpowers

  1. Lightweight processes — 300-400 bytes each; millions concurrent
  2. Preemptive scheduler — multicore-aware; per-process GC
  3. Message passing — no shared memory; actor model
  4. Hot code swap — replace code at runtime without downtime

"Let It Crash"

defmodule Worker do
  use GenServer

  def handle_call(:dangerous_op, _from, state) do
    result = dangerous_call()
    {:reply, result, state}
  end
end

defmodule MyApp.Supervisor do
  use Supervisor

  def init(_) do
    children = [
      {Worker, []},
    ]
    Supervisor.init(children, strategy: :one_for_one, max_restarts: 3, max_seconds: 60)
  end
end

Principle: don't write defensive code — let it fail, let the Supervisor restart. This is the heart of OTP.

Phoenix + LiveView — Realtime UI Without React

defmodule MyAppWeb.CounterLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0)}
  end

  def handle_event("inc", _value, socket) do
    {:noreply, assign(socket, count: socket.assigns.count + 1)}
  end

  def render(assigns) do
    ~H"""
    <div>
      <p>Count: <%= @count %></p>
      <button phx-click="inc">+1</button>
    </div>
    """
  end
end

The server pushes DOM diffs over WebSocket. SPA-class UX without a JS framework. Fly.io, Supabase Realtime, Discord run this at scale.

Real-World Usage

  • WhatsApp — 2 engineers, 2M concurrent users (2012); now 2B users
  • Discord — 5M concurrent per Elixir server
  • Pinterest — notifications on Erlang
  • Goldman Sachs, Klarna, Bleacher Report

Gleam — 2024's 1.0 Typed Language on BEAM

Where Elixir is dynamic, Gleam (Louis Pilfold, since 2016) is statically typed on BEAM. 1.0 shipped March 2024.

import gleam/io

pub type Shape {
  Circle(radius: Float)
  Square(side: Float)
}

pub fn area(shape: Shape) -> Float {
  case shape {
    Circle(r) -> 3.14159 *. r *. r
    Square(s) -> s *. s
  }
}

pub fn main() {
  let c = Circle(radius: 5.0)
  io.debug(area(c))
}
  • Rust-level type inference
  • Bidirectional interop with Elixir/Erlang
  • Also compiles to JavaScript
  • Rust-ish syntax

Clojure, F#, Scala, OCaml — Hybrid FP

Clojure

  • LISP dialect on the JVM (Rich Hickey, 2007)
  • Immutability default, persistent data structures
  • Macros, core.async (CSP)
  • Walmart, Nubank, NASA
(defn add [a b] (+ a b))
(->> [1 2 3 4 5]
     (filter odd?)
     (map #(* % %))
     (reduce +))

F#

  • Microsoft (Don Syme, 2005), OCaml-based on .NET
  • Jet.com, Walmart Labs, finance

Scala

  • OOP + FP hybrid on JVM (Martin Odersky, 2003)
  • Twitter, LinkedIn, Spotify; Akka, Spark

OCaml

  • INRIA, 1996
  • Jane Street, Facebook (Flow, Hack), early Docker, Coq

React, Redux, RxJS — JS's FP Inheritance

React's core idea: UI = f(state).

function Greeting({ name }) {
  return <h1>Hello, {name}</h1>
}

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

Redux is literally reduce:

function reducer(state, action) {
  switch (action.type) {
    case 'INC': return { ...state, count: state.count + 1 }
    case 'DEC': return { ...state, count: state.count - 1 }
    default: return state
  }
}

RxJS is the Observable Monad — streams of values over time:

const click$ = fromEvent(button, 'click').pipe(
  map(e => e.clientX),
  filter(x => x > 100),
  debounceTime(300)
)

Effect-TS — TypeScript's Haskell

1.0 in 2024. A TypeScript port of Scala ZIO.

import { Effect, pipe, Layer } from 'effect'

interface Logger {
  log: (msg: string) => Effect.Effect<never, never, void>
}
const Logger = Context.Tag<Logger>()

const program = Effect.gen(function* () {
  const logger = yield* Logger
  yield* logger.log("starting...")
  const user = yield* fetchUser(1)
  yield* logger.log(`got user ${user.name}`)
  return user
})

const LoggerLive = Layer.succeed(Logger, {
  log: (msg) => Effect.sync(() => console.log(msg))
})

Effect.runPromise(pipe(program, Effect.provide(LoggerLive)))
  • Unifies DI + error handling + async + retry + fibers, type-safe
  • Avoids Monad Transformer hell at large scale
  • Steep learning curve, big productivity payoff for complex systems

Concurrency Models

  • Actor (Erlang/Elixir/Akka) — processes + mailboxes; no shared memory; millions concurrent
  • CSP (Go, Clojure core.async) — channels; "share memory by communicating"
  • STM — memory as DB transactions (Haskell STM, Clojure ref)
  • Promise/Future (JS, Rust, Python, Swift) — futures + async/await

2024-2025 Languages to Watch

  • Roc (Richard Feldman, Elm successor) — 0.1 in 2024, LLVM-based, Platform abstraction
  • Unison — "distributed pure programming"; functions are hashes, transferred by reference
  • Koka (Daan Leijen, Microsoft) — Algebraic Effects, a Monad alternative
  • Lean 4 — math proofs + general programming (Terence Tao uses it)

10 Common Anti-Patterns

  1. Monad hell — wrapping everything destroys readability. Use Monads at the edges.
  2. Pure purity — dodging all I/O is impractical. Concentrate effects at the boundary.
  3. reduce overuse — complex reducers are unreadable. Split into map/filter.
  4. Lazy-eval abuse — hard to debug, leaks memory.
  5. Forced curry — breaks IDE type hints in JS/TS.
  6. Immutability obsession — local mutable vars are fine; enforce immutability at edges.
  7. Too-long chains — 10-step .map.filter.reduce is often clearer imperative.
  8. Monad Transformer stacks — 4-5 layers deep? Consider Effect-TS/Koka.
  9. Actor overuse — making everything an Actor is over-engineering. CPU-heavy work wants data-oriented design.
  10. Dynamic FP — Monad chains without types are hell. Start in a typed language.

Learning Checklist (2025)

  1. Fluent map/filter/reduce in any language
  2. Option/Result in Rust, Swift, Kotlin
  3. Pattern matching + ADTs (TS discriminated union, Rust enum)
  4. Pick an immutable library (Immer, Immutable.js, Clojure, Scala)
  5. Understand Promise chain as Monad
  6. Core RxJS operators — map, filter, mergeMap, switchMap
  7. React Hooks as memoization (useMemo, useCallback)
  8. One Actor or CSP pattern — Elixir GenServer or Go channel
  9. Experiment with Effect-TS or fp-ts
  10. Two-week Haskell exploration — expand your mental model
  11. Elixir + Phoenix LiveView — realtime UI
  12. Monad laws — intuition, not memorization

Next Post — "Why Is Kubernetes So Complex?"

If FP raises the reliability of individual code, Kubernetes and Cloud Native govern the reliability of the whole system. Google open-sourced its Borg experience in 2014 and K8s became the OS of the cloud — with infamous complexity to match.

Next post covers: problems K8s solved and created; Pod/Service/Deployment/Ingress basics; control-plane architecture (etcd, API Server, Scheduler, Controller); networking (CNI, kube-proxy, Service Mesh — Istio, Linkerd, Cilium); sidecar vs Ambient Mesh; GitOps (ArgoCD, Flux); Helm vs Kustomize vs Jsonnet; Platform Engineering (Backstage, IDP); Operator pattern + CRDs; multi-cluster (Karmada, Cluster API, KubeFed); cost (OpenCost, Kubecost, Karpenter); 2025 trends (WASM, eBPF, Gateway API). Starting from the fundamental question — "is Kubernetes really needed?" — through full operational recipes.