@zakkster/mostly-adequate-fp
v1.0.1
Published
The FP toolkit from Professor Frisby's Mostly Adequate Guide — curry, compose, Maybe, Either, IO, Task, and 40+ helpers as a real, importable module.
Maintainers
Readme
@zakkster/mostly-adequate-fp
The FP toolkit from Professor Frisby's Mostly Adequate Guide — as a real, importable module.
The Mostly Adequate Guide is the most-starred functional programming book on GitHub. But it ships zero runnable code. Every reader has to re-implement curry, compose, Maybe, Either, IO, and Task from scratch.
This package is that implementation — complete, tested, tree-shakeable, and ready for production.
npm i @zakkster/mostly-adequate-fpWhat's Inside
| Module | Exports | Purpose |
|--------|---------|---------|
| core | curry, curryN, partial, compose, pipe, trace | The engine |
| data | Identity, Maybe, Either, Left, Right, IO, Task | The containers |
| helpers | map, chain, prop, safeProp, filter, reduce, split, join, … | The vocabulary |
| logic | ifElse, when, unless, cond, allPass, anyPass, defaultTo, … | The branching |
| interop | promiseToTask, taskToPromise, ioOf, nodeToTask | The bridges |
Quick Start
import { compose, Maybe, map, prop, safeProp, split, head, join, toUpperCase } from '@zakkster/mostly-adequate-fp';
// Pure pipeline: extract initials from a name
const getInitial = compose(toUpperCase, head);
const getInitials = compose(join(''), map(getInitial), split(' '));
// Safe wrapper: handles null without if-statements
const safeGetInitials = compose(
map(getInitials),
safeProp('name')
);
safeGetInitials({ name: 'john doe' }).inspect(); // Just(JD)
safeGetInitials({}).inspect(); // Nothing
safeGetInitials(null).inspect(); // Nothing📖 Recipes
Every recipe below is a self-contained example you can copy into your project.
import { compose, pipe, map, filter, reduce, split, join, toUpperCase, head } from '@zakkster/mostly-adequate-fp';
// compose reads bottom-to-top
const shout = compose(
join('!'), // 3. ["HELLO", "WORLD"] → "HELLO!WORLD"
map(toUpperCase), // 2. ["hello", "world"] → ["HELLO", "WORLD"]
split(' ') // 1. "hello world" → ["hello", "world"]
);
shout('hello world'); // "HELLO!WORLD"
// pipe reads top-to-bottom (same thing, reversed)
const sum = pipe(
filter(x => x > 0),
reduce((a, b) => a + b, 0)
);
sum([-1, 2, 3, -4, 5]); // 10import { Maybe, compose, map, safeProp } from '@zakkster/mostly-adequate-fp';
// Without Maybe:
// if (user && user.address && user.address.street) { ... }
// With Maybe:
const getStreet = compose(
map(map(toUpperCase)), // Maybe(Maybe("ELM ST")) → uppercase inner
map(safeProp('street')), // Maybe({ street: ... }) → Maybe(Maybe("elm st"))
safeProp('address') // { address: ... } → Maybe({ street: ... })
);
getStreet({ address: { street: 'elm st' } }).inspect();
// Just(Just(ELM ST))
getStreet({ address: {} }).inspect();
// Just(Nothing)
getStreet(null).inspect();
// Nothing
// Extract with a default
Maybe.of(null).getOrElse('N/A'); // "N/A"
Maybe.of(42).getOrElse('N/A'); // 42import { Either, Right, Left, either, compose, map, prop } from '@zakkster/mostly-adequate-fp';
// Parse JSON safely
const parseJSON = (str) => {
try { return Right.of(JSON.parse(str)); }
catch (e) { return Left.of(e.message); }
};
// Build a pipeline that might fail
const getConfigValue = compose(
map(prop('port')),
parseJSON
);
getConfigValue('{"port": 3000}').inspect(); // Right(3000)
getConfigValue('bad json').inspect(); // Left(Unexpected token...)
// Extract the result
const result = either(
err => `Error: ${err}`, // Left handler
port => `Server on :${port}` // Right handler
);
result(getConfigValue('{"port":3000}')); // "Server on :3000"
result(getConfigValue('oops')); // "Error: Unexpected token..."
// Lift nullable values
Either.fromNullable(42); // Right(42)
Either.fromNullable(null); // Left(null)import { IO, compose, map, prop } from '@zakkster/mostly-adequate-fp';
// Wrap impure operations in IO
const getDocument = new IO(() => document);
const getTitle = compose(map(prop('title')), () => getDocument);
// Nothing has executed yet!
const titleIO = getDocument.map(doc => doc.title);
// Only runs when you explicitly trigger it
titleIO.run(); // "My Page Title"
// Compose multiple IOs
const getLocalStorage = (key) => new IO(() => localStorage.getItem(key));
const parseSettings = compose(
map(JSON.parse),
getLocalStorage
);
// Still lazy — no localStorage access yet
const settingsIO = parseSettings('app-settings');
settingsIO.run(); // { theme: "dark", lang: "en" }import { Task, compose, map, prop } from '@zakkster/mostly-adequate-fp';
import { promiseToTask, taskToPromise } from '@zakkster/mostly-adequate-fp/interop';
// Wrap fetch in a Task (lazy — no request until .fork())
const fetchUser = (id) => new Task((reject, resolve) => {
fetch(`/api/users/${id}`)
.then(r => r.ok ? r.json() : Promise.reject('Not found'))
.then(resolve)
.catch(reject);
});
// Build a pure pipeline
const getUserGreeting = compose(
map(name => `Hello, ${name}!`),
map(prop('name')),
fetchUser
);
// Nothing happens until fork()
const greeting = getUserGreeting(1);
greeting.fork(
err => console.error(err),
html => console.log(html) // "Hello, Alice!"
);
// Convert existing Promise functions
const fetchTask = promiseToTask(fetch);
const task = fetchTask('/api/data').map(r => r.json());
// Convert Task back to Promise (for async/await interop)
const data = await taskToPromise(Task.of(42)); // 42import { ifElse, when, unless, cond, allPass, anyPass, defaultTo, T, compose, prop, eq } from '@zakkster/mostly-adequate-fp';
// ifElse: functional if/else
const classifyTemp = ifElse(
t => t > 30,
() => 'hot',
() => 'cold'
);
classifyTemp(35); // "hot"
classifyTemp(10); // "cold"
// cond: functional switch/case
const httpMessage = cond([
[eq(200), () => 'OK'],
[eq(404), () => 'Not Found'],
[eq(500), () => 'Server Error'],
[T, (code) => `Unknown: ${code}`],
]);
httpMessage(404); // "Not Found"
// allPass / anyPass: combine predicates
const isValidUser = allPass([
u => u.age >= 18,
u => u.email.includes('@'),
u => u.name.length > 0,
]);
isValidUser({ name: 'Zak', age: 25, email: '[email protected]' }); // true
// defaultTo: null-safe defaults
defaultTo(0, null); // 0
defaultTo(0, undefined); // 0
defaultTo(0, NaN); // 0
defaultTo(0, 42); // 42
// when: conditional transform
const doubleIfPositive = when(x => x > 0, x => x * 2);
doubleIfPositive(5); // 10
doubleIfPositive(-3); // -3import { curry, curryN, partial } from '@zakkster/mostly-adequate-fp';
// Auto-curry any function
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6
add(1)(2, 3); // 6
add(1, 2, 3); // 6
// curryN for functions with default params (fn.length lies)
const fetch3 = curryN(3, (url, method = 'GET', headers = {}) => { /* ... */ });
fetch3('/api')('POST')({ 'Content-Type': 'application/json' });
// partial for strict binding (no further currying)
const greet = (greeting, title, name) => `${greeting}, ${title} ${name}!`;
const sayHello = partial(greet, 'Hello', 'Dr.');
sayHello('Smith'); // "Hello, Dr. Smith!"import { promiseToTask, taskToPromise, ioOf, nodeToTask } from '@zakkster/mostly-adequate-fp/interop';
import { compose, map, prop } from '@zakkster/mostly-adequate-fp';
// Promise → Task (makes it lazy)
const fetchTask = promiseToTask(fetch);
const getData = compose(
map(r => r.json()),
fetchTask
);
// Nothing fetched yet! Only fires on .fork()
getData('/api/data').fork(console.error, console.log);
// Task → Promise (for async/await interop)
async function main() {
const result = await taskToPromise(Task.of(42));
// result === 42
}
// IO helper
const readStorage = ioOf(() => localStorage.getItem('token'));
readStorage.run(); // only reads when triggered
// Node callback → Task
const readFile = nodeToTask(fs.readFile);
readFile('./data.json', 'utf8')
.map(JSON.parse)
.map(prop('users'))
.fork(console.error, console.log);Module Imports
Import only what you need — everything is tree-shakeable:
// Specific modules
import { curry, compose } from '@zakkster/mostly-adequate-fp/core';
import { Maybe, Either, Task } from '@zakkster/mostly-adequate-fp/data';
import { map, safeProp } from '@zakkster/mostly-adequate-fp/helpers';
import { ifElse, cond } from '@zakkster/mostly-adequate-fp/logic';
import { promiseToTask } from '@zakkster/mostly-adequate-fp/interop';
// Or everything at once
import { curry, compose, Maybe, map, ifElse, Task } from '@zakkster/mostly-adequate-fp';Algebraic Laws
Every container in this library satisfies the standard algebraic laws:
| Law | Test |
|-----|------|
| Functor Identity | m.map(x => x) ≡ m |
| Functor Composition | m.map(f).map(g) ≡ m.map(x => g(f(x))) |
| Monad Left Identity | M.of(a).chain(f) ≡ f(a) |
| Monad Right Identity | m.chain(M.of) ≡ m |
| Applicative Identity | A.of(x => x).ap(v) ≡ v |
Attribution
This library is derived from Professor Frisby's Mostly Adequate Guide to Functional Programming by Brian Lonsdorf.
Licensed under CC BY-SA 4.0.
