.claude/skills/functional/SKILL.md
Functional programming patterns with immutable data. Use when writing logic, data transformations, or encountering mutation bugs. Covers immutability violations catalog, pure functions, composition, early returns, and options objects. Do NOT over-apply heavy FP abstractions (monads, fp-ts) unless the project requires them.
npx skillsauth add jscriptcoder/jshack.me functionalInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Immutable data is the foundation of functional programming. Understanding WHY helps you embrace it:
Example of the problem:
// ❌ WRONG - Mutation creates unpredictable behavior
const machine = { ip: '10.0.1.5', ports: [{ port: 22, service: 'ssh', open: true }] };
addPort(machine, { port: 80, service: 'http', open: true }); // Mutates machine.ports internally
console.log(machine.ports.length); // 2 - SURPRISE! machine changed
// ✅ CORRECT - Immutable approach is predictable
const machine = { ip: '10.0.1.5', ports: [{ port: 22, service: 'ssh', open: true }] };
const updatedMachine = addPort(machine, { port: 80, service: 'http', open: true }); // Returns new object
console.log(machine.ports.length); // 1 - original unchanged
console.log(updatedMachine.ports.length); // 2 - new version
We follow "Functional Light" principles - practical functional patterns without heavy abstractions:
What we DO:
What we DON'T do:
Why: The goal is maintainable, testable code - not academic purity. If a functional pattern makes code harder to understand, don't use it.
Example - Keep it simple:
// ✅ GOOD - Simple, clear, functional
const openPorts = machine.ports.filter((p) => p.open);
const serviceNames = openPorts.map((p) => p.service);
// ❌ OVER-ENGINEERED - Unnecessary abstraction
const compose =
<T>(...fns: Array<(arg: T) => T>) =>
(x: T) =>
fns.reduceRight((v, f) => f(v), x);
const serviceNames = compose(
filter((p: Port) => p.open),
map((p: Port) => p.service),
)(machine.ports);
Code should be clear through naming and structure. Comments that restate what the code does are noise. Comments that explain why — the intent, the non-obvious constraint, the business reason — are valuable.
❌ WRONG - Comments explaining unclear code
// Get the node and check if it's a file with read access
function check(n: any) {
// Check node exists
if (n) {
// Check if file
if (n.t) {
// Check permission
if (n.p) {
return true;
}
}
}
return false;
}
✅ CORRECT - Self-documenting code, no comments needed
function canReadFile(file: FileNode | undefined, user: UserType): boolean {
if (!file) return false;
if (file.type !== 'file') return false;
if (!file.permissions.read.includes(user)) return false;
return true;
}
✅ CORRECT - Comment explains WHY, not what
function findBinary(name: string, machine: string, getNode: GetNodeFn): FileNode | null {
// Check cwd first so locally-placed scripts override system binaries
const cwdBinary = getNode(machine, `${currentPath}/${name}`, '/');
if (cwdBinary) return cwdBinary;
const binBinary = getNode(machine, `/bin/${name}`, '/');
if (binBinary) return binBinary;
return getNode(machine, `/usr/bin/${name}`, '/');
}
✅ CORRECT - Comment explains a non-obvious constraint
const resolveNat = (ip: string, port: number): string | null => {
// NAT rules on the router use iptables DNAT — we must match the
// external port to the internal mapping, not the service port
const rule = iptablesRules.find((r) => r.externalPort === port);
if (!rule) return null;
return rule.internalIp;
};
Do comment when:
Don't comment when:
If code needs what comments, refactor instead:
✅ Acceptable JSDoc for public APIs
/**
* Creates a seeded PRNG for deterministic mission generation.
* @param seed - The mission seed string used to derive the initial state
* @throws {Error} if seed is empty
*/
export function createPrng(seed: string): Prng {
// Implementation
}
Prefer map, filter, reduce for transformations. They're declarative (what, not how) and naturally immutable.
❌ WRONG - Imperative loop
const serviceNames = [];
for (const port of machine.ports) {
serviceNames.push(port.service);
}
✅ CORRECT - Functional map
const serviceNames = machine.ports.map((p) => p.service);
❌ WRONG - Imperative loop
const openPorts = [];
for (const port of machine.ports) {
if (port.open) {
openPorts.push(port);
}
}
✅ CORRECT - Functional filter
const openPorts = machine.ports.filter((p) => p.open);
❌ WRONG - Imperative loop
let totalMachines = 0;
for (const layer of mission.layers) {
totalMachines += layer.machines.length;
}
✅ CORRECT - Functional reduce
const totalMachines = mission.layers.reduce((sum, layer) => sum + layer.machines.length, 0);
✅ CORRECT - Compose array methods
const sshTargets = network.machines
.filter((m) => !m.bricked)
.map((m) => m.ports.filter((p) => p.port === 22 && p.open))
.reduce((all, ports) => [...all, ...ports], []);
Imperative loops are fine when:
for...of with break)But even then, consider:
Array.find() for early terminationArray.some() / Array.every() for boolean checksDefault to options objects for function parameters. This improves readability and reduces ordering dependencies.
Benefits:
❌ WRONG - Positional parameters
function generateMission(
seed: string,
difficulty: Difficulty,
entryVariant: EntryVariant,
networkMode: NetworkMode,
domainEntry: boolean,
gpgEnabled: boolean,
): MissionNetwork {
// ...
}
// Call site - unclear what parameters mean
generateMission('abc123', 'hard', 'ssh', 'nat', true, false);
✅ CORRECT - Options object
type GenerateMissionOptions = {
readonly seed: string;
readonly difficulty: Difficulty;
readonly entryVariant: EntryVariant;
readonly networkMode: NetworkMode;
readonly domainEntry?: boolean;
readonly gpgEnabled?: boolean;
};
function generateMission(options: GenerateMissionOptions): MissionNetwork {
const {
seed,
difficulty,
entryVariant,
networkMode,
domainEntry = false,
gpgEnabled = false,
} = options;
// ...
}
// Call site - crystal clear
generateMission({
seed: 'abc123',
difficulty: 'hard',
entryVariant: 'ssh',
networkMode: 'nat',
domainEntry: true,
});
Use positional parameters when:
add(a, b))// ✅ OK - Obvious ordering, few parameters
function resolvePath(path: string, cwd: string): string {
return normalizePath(`${cwd}/${path}`);
}
function addChild(parent: FileNode, child: FileNode): FileNode {
return { ...parent, children: { ...parent.children, [child.name]: child } };
}
Pure functions have no side effects and always return the same output for the same input.
No side effects
Deterministic
Referentially transparent
❌ WRONG - Impure function (mutations)
function addMachine(machines: GeneratedMachine[], newMachine: GeneratedMachine): void {
machines.push(newMachine); // ❌ Mutates input
}
let portCount = 0;
function countPort(): number {
portCount++; // ❌ Modifies external state
return portCount;
}
✅ CORRECT - Pure functions
function addMachine(
machines: ReadonlyArray<GeneratedMachine>,
newMachine: GeneratedMachine,
): ReadonlyArray<GeneratedMachine> {
return [...machines, newMachine]; // ✅ Returns new array
}
function countPort(current: number): number {
return current + 1; // ✅ No external state
}
Some functions must be impure (I/O, randomness, side effects). Isolate them:
// ✅ CORRECT - Isolate impure functions at edges
// Pure core
function countOpenPorts(machine: Readonly<RemoteMachine>): number {
return machine.ports.reduce((sum, p) => sum + (p.open ? 1 : 0), 0);
}
// Impure shell (isolated)
function persistPatch(patch: FileSystemPatch): void {
const count = countOpenPorts(machine); // Pure
indexedDB.put('patches', patch); // Impure (I/O)
}
Pattern: Keep impure functions at system boundaries (adapters, ports). Keep core domain logic pure.
Compose small functions into larger ones. Each function does one thing well.
❌ WRONG - Complex monolithic function
function validateMissionSeed(input: unknown) {
if (typeof input !== 'object' || !input) {
throw new Error('Invalid input');
}
if (!('seed' in input) || typeof input.seed !== 'string') {
throw new Error('Missing seed');
}
if (!('difficulty' in input) || typeof input.difficulty !== 'string') {
throw new Error('Missing difficulty');
}
if (!('machines' in input) || !Array.isArray(input.machines)) {
throw new Error('Missing machines');
}
// ... 50 more lines of validation and registration
}
✅ CORRECT - Composed functions
// Small, focused functions
const validate = (input: unknown) => MissionSeedSchema.parse(input);
const generate = (config: MissionConfig) => generateMissionNetwork(config);
// Compose them
const createMission = (input: unknown) => generate(validate(input));
// Small transformation functions
const addUsers = (
machine: GeneratedMachine,
users: ReadonlyArray<RemoteUser>,
): GeneratedMachine => ({
...machine,
users,
});
const addPorts = (machine: GeneratedMachine, ports: ReadonlyArray<Port>): GeneratedMachine => ({
...machine,
ports,
});
const setRole = (machine: GeneratedMachine, role: MachineRole): GeneratedMachine => ({
...machine,
role,
});
// Compose them
const enrichMachine = (machine: GeneratedMachine, prng: Prng): GeneratedMachine => {
return setRole(
addPorts(addUsers(machine, generateUsers(prng)), generatePorts(machine.role, prng)),
machine.role,
);
};
Use readonly on all data structures to signal immutability intent.
// ✅ CORRECT - Immutable data structure
type RemoteMachine = {
readonly ip: string;
readonly hostname: string;
readonly ports: ReadonlyArray<Port>;
};
// ❌ WRONG - Mutable
type RemoteMachine = {
ip: string;
hostname: string;
};
// ✅ CORRECT - Immutable array
type MissionNetwork = {
readonly machines: ReadonlyArray<GeneratedMachine>;
};
// ❌ WRONG - Mutable array
type MissionNetwork = {
readonly machines: GeneratedMachine[];
};
// ✅ CORRECT - Deep immutability
type Port = {
readonly port: number;
readonly service: string;
readonly open: boolean;
readonly vulnerability?: {
readonly type: string;
readonly payload: readonly string[];
};
};
Max 2 levels of function nesting. Beyond that, extract functions.
❌ WRONG - Deep nesting (4+ levels)
function checkFileAccess(node: FileNode, path: string, user: UserType) {
if (node.type === 'directory') {
if (node.children) {
if (path in node.children) {
if (node.children[path].permissions.read.includes(user)) {
// ... deeply nested logic
}
}
}
}
}
✅ CORRECT - Flat with early returns
function checkFileAccess(node: FileNode, path: string, user: UserType) {
if (node.type !== 'directory') return false;
if (!node.children) return false;
if (!(path in node.children)) return false;
if (!node.children[path].permissions.read.includes(user)) return false;
// Main logic at top level
return true;
}
✅ CORRECT - Extract to functions
function checkFileAccess(node: FileNode, path: string, user: UserType) {
if (!isAccessibleDirectory(node, path)) return false;
const child = node.children![path];
return hasReadPermission(child, user);
}
function isAccessibleDirectory(node: FileNode, path: string): boolean {
return node.type === 'directory' && node.children !== undefined && path in node.children;
}
Complete catalog of array mutations and their immutable alternatives:
// ❌ WRONG - Mutations
machines.push(newMachine); // Add to end
machines.pop(); // Remove last
machines.unshift(newMachine); // Add to start
machines.shift(); // Remove first
machines.splice(index, 1); // Remove at index
machines.reverse(); // Reverse order
machines.sort(); // Sort
machines[i] = newValue; // Update at index
// ✅ CORRECT - Immutable alternatives
const withNew = [...machines, newMachine]; // Add to end
const withoutLast = machines.slice(0, -1); // Remove last
const withFirst = [newMachine, ...machines]; // Add to start
const withoutFirst = machines.slice(1); // Remove first
const removed = [
...machines.slice(0, index), // Remove at index
...machines.slice(index + 1),
];
const reversed = [...machines].reverse(); // Reverse (copy first!)
const sorted = [...machines].sort(); // Sort (copy first!)
const updated = machines.map(
(
m,
idx, // Update at index
) => (idx === i ? newValue : m),
);
Common patterns:
// Filter out specific machine by IP
const withoutMachine = machines.filter((m) => m.ip !== targetIp);
// Replace specific machine
const replaced = machines.map((m) => (m.ip === targetIp ? updatedMachine : m));
// Insert at specific position
const inserted = [...machines.slice(0, index), newMachine, ...machines.slice(index)];
// ❌ WRONG
machine.hostname = 'web-server';
Object.assign(machine, { hostname: 'web-server' });
// ✅ CORRECT
const updated = { ...machine, hostname: 'web-server' };
// ✅ CORRECT - Immutable nested update
const updatedMachine = {
...machine,
ports: machine.ports.map((p, i) => (i === targetIndex ? { ...p, open: false } : p)),
};
// ✅ CORRECT - Immutable nested array update (filesystem tree)
const updatedRoot = {
...root,
children: {
...root.children,
[childName]: updatedChild,
},
};
// ❌ WRONG - Nested conditions
if (node) {
if (node.type === 'file') {
if (node.permissions.read.includes(userType)) {
// do something
}
}
}
// ✅ CORRECT - Early returns (guard clauses)
if (!node) return;
if (node.type !== 'file') return;
if (!node.permissions.read.includes(userType)) return;
// do something
type PermissionResult =
| { readonly allowed: true }
| { readonly allowed: false; readonly reason: string };
// Usage
function checkTraversal(fs: FileNode, path: string, user: UserType): PermissionResult {
if (!fs.children) {
return { allowed: false, reason: `${path}: No such file or directory` };
}
const segment = path.split('/')[0];
if (!fs.children[segment]?.permissions.execute.includes(user)) {
return { allowed: false, reason: `${path}: Permission denied` };
}
return { allowed: true };
}
// Caller handles both cases explicitly
const result = checkTraversal(fs, path, user);
if (!result.allowed) {
return result.reason;
}
// TypeScript knows result.allowed is true here
When writing functional code, verify:
map, filter, reduce) over loopsreadonly on all data structure propertiesReadonlyArray<T> for immutable arraysdevelopment
TypeScript strict mode patterns including schema-first development, branded types, type vs interface guidance, and tsconfig strict flags. Use when writing TypeScript code, defining types or schemas, or reviewing type safety. For immutability and pure function patterns, see the functional skill.
development
Testing patterns for behavior-driven tests. Use when writing tests, creating test factories, structuring test files, or deciding what to test. Do NOT use for UI-specific testing (see front-end-testing or react-testing skills).
testing
Evaluates test quality using Dave Farley's 8 properties. Use when reviewing tests, assessing test suite quality, or analyzing test effectiveness against TDD best practices.
development
Test-Driven Development workflow. Use for ALL code changes - features, bug fixes, refactoring. TDD is non-negotiable.