Category theory often sounds like impenetrable academic jargon, but at its core, it is simply the mathematics of composition. In functional programming, we use its concepts to build modular, predictable, and bug-resistant code. JavaScript isn't a pure functional language like Haskell, but it has first-class functions and treats functions as values. This makes it entirely possible to implement category theory primitives. Here is how the theoretical concepts map to practical JavaScript. The Vocabulary of Category Theory Before jumping into code, let's define the fundamental pieces: Categories: A collection of objects and arrows (morphisms) between them. Objects: In programming, these are our types or data (e.g., String, Number, Boolean, Array). Morphisms: These are our pure functions that transform one type into another (e.g., a function that takes a String and returns a Number). The goal of functional programming primitives is to create safe wrappers (containers) around our data so we can compose these functions predictably, without side effects or unhandled errors. 1. Functors: The Mappables A Functor is any type that implements a map method. It is a container holding a value, and map allows you to apply a function to that value without pulling it out of the container. When you map over a Functor, it returns a new Functor of the same type, preserving the container's structure. The native JS Functor: You already use Functors every day. JavaScript Arrays are Functors. const numbers = [1, 2, 3]; // We apply a function to the values inside, and get a new Array back const doubled = numbers.map(x => x * 2); // [2, 4, 6] Enter fullscreen mode Exit fullscreen mode Building a custom Functor: Let's build a simple Box container to see how this works for single values. const Box = x => ({ map: f => Box(f(x)), fold: f => f(x), // An escape hatch to get the value out inspect: () => `Box(${x})` }); // Usage: const result = Box(' Functional Programming ') .map(str => str.trim()) .map(str => str.toLowerCase()) .map(str => str.split(' ')); console.log(result.inspect()); // Box(Array(2)) console.log(result.fold(x => x)); // ['functional', 'programming'] Enter fullscreen mode Exit fullscreen mode Because map always returns a Box, we can chain operations infinitely. 2. Monoids: The Combiners A Monoid is a type that has two things: An identity value: A value that, when combined with another value, doesn't change it (like 0 for addition, or "" for strings). An associative binary operation: A function that combines two values of the same type into one (like + or concat()). "Associative" just means the grouping doesn't matter: (a + b) + c === a + (b + c). The native JS Monoids: Strings and Numbers are standard monoids under specific operations. // String Monoid (Identity is "") const strConcat = "hello" + ""; // "hello" // Number Monoid for Addition (Identity is 0) const numAdd = 5 + 0; // 5 // Array Monoid (Identity is []) const arrConcat = [1, 2].concat([]); // [1, 2] Enter fullscreen mode Exit fullscreen mode Monoids are incredibly powerful for reducing a list of things into a single thing. This is exactly what Array.prototype.reduce relies on. 3. Monads: The Flatteners The infamous Monad. The standard joke is that "a monad is just a monoid in the category of endofunctors," which is true mathematically but useless for programming. Practically, a Monad is a Functor that also implements a flatMap (or chain, or bind) method. The Problem Monads Solve: If you have a function that returns a Box, mapping it over a Box results in a nested container: Box(Box(value)). A Monad knows how to flatten itself to prevent this nesting. // A function that returns a container const half = x => (x % 2 === 0 ? Box(x / 2) : Box("Odd")); // Using map creates nested Boxes const nested = Box(4).map(half); console.log(nested.inspect()); // Box(Box(2)) Enter fullscreen mode Exit fullscreen mode Adding Monadic Behavior: To make our Box a Monad, we add chain (the JS convention for flatMap). const MonadicBox = x => ({ map: f => MonadicBox(f(x)), chain: f => f(x), // Instead of re-boxing, we just return the result of f(x) inspect: () => `Box(${x})` }); const flatResult = MonadicBox(4).chain(half); console.log(flatResult.inspect()); // Box(2) - No nesting! Enter fullscreen mode Exit fullscreen mode The native JS Monads: Array.prototype.flatMap makes Arrays monads. Promise is heavily inspired by Monads. Calling .then() with a function that returns a new Promise automatically flattens the result. You never get Promise>. 4. Applicatives: The Multi-Container Operators What happens if you have a function wrapped in a Functor, and you want to apply it to a value wrapped in another Functor? A standard Functor's map expects a normal function, not a wrapped one. An Applicative implements an ap method to solve this. const AppBox = x => ({ map: f => AppBox(f(x)), ap: otherBox => otherBox.map(x), // x here is actually a function! inspect: () => `Box(${x})` }); const boxWithFunction = AppBox(x => x + 10); const boxWithValue = AppBox(5); // Applies the wrapped function to the wrapped value const result = boxWithFunction.ap(boxWithValue); console.log(result.inspect()); // Box(15) Enter fullscreen mode Exit fullscreen mode Summary of Primitives Here is a quick reference guide to how these primitives build on top of each other: Primitive Core Interface Purpose Functor map Apply a function to a wrapped value. Applicative ap Apply a wrapped function to a wrapped value. Monad chain / flatMap Sequence operations that return containers, preventing nesting. Monoid concat + empty Combine values of the same type safely.
Functional programming primitives in Javascript
Full Article
📰 Original Source
Read full article at Dev →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.