LabHub

Blog

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

한국어English日本語

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:

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.

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).

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:

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:

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

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

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

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))
}

Clojure, F#, Scala, OCaml — Hybrid FP

Clojure

(defn add [a b] (+ a b))
(->> [1 2 3 4 5]
     (filter odd?)
     (map #(* % %))
     (reduce +))

F#

Scala

OCaml

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)))

Concurrency Models

2024-2025 Languages to Watch

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.

Comments

No comments yet.

Sign in to leave a comment