opinionated-software-engineering/skills/functional-programmer/SKILL.md
Functional programming principles, patterns, and practices. Use when working with functional languages (Racket, Clojure, Erlang, Haskell, Idris, Scheme, OCaml, F#, Elixir, etc.) without language-specific skills available, or when applying functional paradigms in multi-paradigm languages.
npx skillsauth add pyroxin/opinionated-claude-skills functional-programmerInstall 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.
This skill provides guidance on functional programming principles, patterns, and practices. Functional programming treats computation as the evaluation of mathematical functions, emphasizing immutability, pure functions, and declarative style. This skill serves as a foundation when working with functional languages or applying functional paradigms in multi-paradigm codebases.
Use this skill when:
Note: Language-specific skills (e.g., clojure-programmer, racket-programmer) supersede this skill when available.
Functional programming views programs as compositions of mathematical functions. A function always produces the same output for the same input, with no hidden state or side effects. This mathematical purity enables powerful reasoning about code behavior.
Quote to remember: "Programs must be written for people to read, and only incidentally for machines to execute." — Harold Abelson, SICP
Rather than modifying data in place, functional programming creates new data structures through transformation. This immutability enables:
Build complex behavior by composing simple functions. Small, single-purpose functions combine to create sophisticated systems. Composition is the fundamental abstraction mechanism in functional programming.
Data structures cannot be modified after creation. Instead, transformations produce new structures. This makes time and change explicit rather than hidden.
Quote to remember: "Time is a device that was invented to keep everything from happening at once." — Ray Cummings (1922), often misattributed
Why immutability matters:
Implementation: Persistent data structures with structural sharing, copy-on-write, or immutable-by-convention.
Pure functions always return the same output for the same input, with no side effects. This referential transparency enables equational reasoning about code.
Why purity matters:
Managing effects: Push side effects to program boundaries. Separate pure core logic from effectful actions. Use effect systems when appropriate.
Functions as values enable abstracting over patterns, not just data. This is more powerful than data abstraction alone.
Why higher-order functions matter:
Express what to compute, not how. Describe desired results rather than step-by-step procedures. This shifts from mechanical instructions to logical assertions about the result.
Why declarative style matters:
<fp_decision_framework>
From the software-engineer skill, use functional approaches when:
Key principles:
<paradigm_decision_table>
| Situation | FP Strength | Consider Alternative When | |-----------|-------------|---------------------------| | Data pipelines | Composition, immutability | Complex branching logic needed | | Concurrent systems | No shared mutable state | Inherently stateful (games, GUIs) | | Correctness-critical | Equational reasoning, testing | Performance-critical tight loops | | Reusable operations | Higher-order functions | Team unfamiliar with FP | | Domain modeling | ADTs, pattern matching | Extensible data (expression problem) | | Parsing/transformation | Declarative specification | Complex imperative protocols | | Mathematical computation | Pure functions match math | I/O-heavy applications | </paradigm_decision_table> </fp_decision_framework>
<fp_thinking_patterns>
<data_transformation>
Prefer expressing operations as data transformations (map/filter/reduce) rather than control flow (loops/conditionals):
When transformation patterns work well:
When explicit control flow may be clearer:
<recursion_tradeoffs>
Recursion is elegant for naturally recursive structures but has costs:
Prefer recursion for:
Consider alternatives when:
Tail recursion enables constant-space recursion in languages that support it. Use trampolining to simulate TCO when unavailable. </recursion_tradeoffs>
<composition_architecture>
Build complex behavior by composing simple functions. Composition is more than a pattern—it's an architectural principle.
Composition works well when:
Composition challenges:
Partial application and currying enable building specialized functions from general ones. Use when creating function pipelines or delaying argument provision. Don't curry everything—clarity matters more than cleverness. </composition_architecture>
<algebraic_data_types>
Make invalid states unrepresentable through sum types (variants) and product types (tuples/records):
Power of sum types:
When to use:
<monad_guidance>
Monads are powerful but often unnecessary. They solve specific problems—don't reach for them reflexively.
Good uses of monads:
Maybe/Option — Avoiding null checks and making absence explicitEither/Result — Error handling as values (when exceptions aren't idiomatic)IO — Isolating side effects in pure languagesState — Threading state through pure computationsList — Non-deterministic computationDon't use monads when:
The monad tutorial fallacy: Monads aren't mysterious. They're patterns for chaining operations that return wrapped values. Use them when that's useful; don't force them. </monad_guidance> </fp_thinking_patterns>
Functional programming is powerful but not universally optimal. Avoid forcing FP in these situations:
Performance-critical tight loops:
Inherently stateful domains:
Team and ecosystem constraints:
Context over dogma: Choose the right tool for the job. FP is one paradigm among many.
Closures and objects are fundamentally equivalent mechanisms for encapsulating state with behavior:
Closures capture environment and expose functions:
makeCounter() {
let count = 0
return {
increment: () => ++count,
get: () => count
}
}
Objects store state and expose methods:
class Counter {
private count = 0
increment() { return ++this.count }
get() { return this.count }
}
Both bind data to code; the difference is syntax and convention. This equivalence reveals:
Quote to remember: "Objects are a poor man's closures" (and vice versa) — attributed to Norman Adams; the inverse attributed to Christian Queinnec. Choose based on idiom, not dogma.
<state_management>
Pure functions can't perform I/O or maintain state. Real programs need both.
State management strategies:
Coeffects (dual to effects/monads) track how programs interact with their execution context.[^1] While monads track computational effects (what a program does to the world), coeffects track computational requirements (what a program needs from its environment).
The core problem: How to keep functions pure while accessing external context (current time, localStorage, database, GPS sensors). Coeffects make these dependencies explicit rather than hidden side effects.
Coeffects as dependency injection: Coeffects are fundamentally a form of dependency injection with a type-theoretic foundation. Traditional DI passes dependencies via constructors/parameters; coeffects declare dependencies in metadata or types, and the framework injects them into a context map. Both make dependencies explicit, improve testability (inject mocks), and invert control (caller provides what callee needs).
Examples of coeffect systems:
:now or :local-store)Practical use: Coeffects work in both typed (using indexed comonads) and dynamic languages (using data injection patterns). They're valuable when you need explicit, testable environmental dependencies rather than hidden global access. However, they're an advanced pattern—apply them when environmental dependencies need to be explicit and verifiable, not as a general replacement for simpler state management.
[^1]: Tomas Petricek, Dominic Orchard, and Alan Mycroft. 2014. Coeffects: A calculus of context-dependent computation. In Proceedings of the 19th ACM SIGPLAN International Conference on Functional Programming (ICFP '14). https://doi.org/10.1145/2628136.2628160 </coeffects>
Real systems combine pure and impure code. The goal is to maximize the pure portion while handling effects and context systematically. </state_management>
Functional programming patterns can have performance implications:
Persistent data structures:
Lazy evaluation:
Recursion:
Trade-off: Functional code prioritizes correctness and maintainability over raw performance. Optimize hot paths when profiling shows the need.
Pure functions are easier to test but can be harder to debug:
Debugging strategies:
Common debugging challenges:
Mitigation: Write small, well-tested functions. Use types to catch errors early. Test compositions separately from components.
Many languages support FP without being purely functional (Java, Python, JavaScript, Swift):
Applying FP in multi-paradigm languages:
Practical FP:
Don't be a zealot: Use FP where it improves code. Accept imperative patterns where they're clearer or more efficient.
Functional programming enables powerful abstractions, but abstraction has costs:
Abstraction benefits:
Abstraction costs:
Guidelines:
Kernighan's Law: "Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian Kernighan
Quote to remember: "Fools ignore complexity. Pragmatists suffer it. Some can avoid it. Geniuses remove it." — Alan Perlis
The expression problem[^2]: how to add new data variants and new operations without modifying existing code?
Object-oriented approach: Easy to add new data types (subclasses), hard to add new operations (requires modifying all classes).
Functional approach: Easy to add new operations (new functions), hard to add new data variants (requires modifying all pattern matches).
Solutions:
Know which dimension you'll extend: If adding data types is common, consider OO. If adding operations is common, consider FP.
[^2]: Philip Wadler. 1998. The Expression Problem. Email to the Java Genericity mailing list. http://homepages.inf.ed.ac.uk/wadler/papers/expression/expression.txt
<common_pitfalls>
<pointfree_overuse>
Pointfree (tacit) style omits function parameters:
sum = reduce(add, 0) # pointfree
sum = λ(xs) → reduce(add, 0, xs) # with points
Pitfall: Pointfree code can become unreadable when overused.
Guideline: Use pointfree for simple compositions. Add explicit parameters when clarity improves. </pointfree_overuse>
<recursion_overuse>
Pitfall: Recursion for every loop leads to stack overflow in languages without tail call optimization.
Guideline:
map, filter, reduce) instead of explicit recursion<strictness_laziness>
Pitfall: Assuming all functional languages behave the same regarding evaluation order.
Lazy languages (Haskell): Expressions evaluate only when needed. Strict languages (most others): Expressions evaluate immediately.
Guideline: Understand your language's evaluation model. Lazy evaluation enables infinite data structures but can cause space leaks. Strict evaluation is predictable but evaluates everything. </strictness_laziness>
<forced_purity>
Pitfall: Trying to eliminate all side effects leads to convoluted code.
Guideline: Separate pure core logic from effectful boundaries. Accept that real programs perform I/O. Use language idioms for effects rather than fighting them. </forced_purity>
<premature_abstraction>
Pitfall: Creating overly generic abstractions before understanding the problem.
Guideline: Follow the rule of three — abstract after seeing a pattern three times. Let abstractions emerge from concrete code rather than designing them upfront. </premature_abstraction>
<ignoring_performance>
Pitfall: Assuming immutability and higher-order functions have no performance cost.
Guideline: Profile before optimizing. Most FP patterns are fast enough. When performance matters, use mutable data structures in hot paths (wrapped in pure interfaces if needed). </ignoring_performance> </common_pitfalls>
<common_mistakes_by_background>
Programmers coming from OOP or imperative languages often import patterns that work against functional principles:
<from_oop_background> From Object-Oriented Programming:
Creating class-like structures when simpler patterns suffice:
Over-engineering with manager/helper patterns:
UserManager, DataHelper classes (actually closure bundles)Not embracing immutability:
Thinking in objects-with-methods instead of data-with-operations:
<from_imperative_background> From Imperative Programming:
Writing procedural code with assignments:
Forcing sequential thinking onto parallel operations:
Using loops instead of collection operations:
Early returns and breaks:
Not thinking about referential transparency:
Mutation creep:
When language-specific skills are available, they provide deeper expertise:
Use language-specific skills when available for idiomatic guidance.
Functional programming emphasizes:
Apply FP where it improves code clarity, correctness, and maintainability. Balance functional purity with practical concerns. Use FP as one tool in your programming toolkit, not a dogmatic requirement.
data-ai
--- name: interactive-research description: Multi-source research orchestrated across a persistent agent team. Use for queries asking for research-style investigation of a topic — comparing, contrasting, surveying, investigating, evaluating, deeply researching, doing a literature review, or any similar research intent — or that ask what people are saying publicly about a topic (discourse, expert opinion, industry consensus, and related framings). Example phrasings (illustrative, not exhaustive):
development
Expert-level guidance for creating high-quality Claude Code skills. Use alongside Anthropic's skill-creator when creating new skills, improving existing skills, or needing guidance on skill content quality. Complements basic skill mechanics with research-driven content development, XML tag structuring, decision frameworks over mechanics, cross-references between skills, and systematic validation.
development
--- name: deep-research description: Deprecated name; the skill formerly here is now opinionated-research:interactive-research. Retained as a context-level notice because Claude Code now ships a built-in deep-research workflow that runs one-shot, non-interactive fan-out research (no conversation after the report). interactive-research instead runs a persistent agent team you can keep questioning once the report lands. Guidance for Claude: when the user makes a research request, ask whether they
development
Pedagogical framework for teaching programming through Socratic dialogue. Use when a learner wants to LEARN programming rather than have code written for them. Triggers include "teach me", "help me understand", "I'm learning", "tutor mode", or requests to not provide solutions. Emphasizes productive struggle, graduated hints, metacognitive scaffolding, and emotional support.