Mostly Adequate Guide to Functional Programming

repository·master·Indexed 12 days ago

https://github.com/mostlyadequate/mostly-adequate-guide

A guide to the functional programming paradigm using JavaScript. It provides exercises, algebraic data structures, and the @mostly-adequate/support library to help developers transition from imperative to functional styles. The guide covers core primitives and algebraic structures including Maybe, Either, IO, Task, List, Map, and Identity, along with functional utilities like compose, curry, and liftA2.

Tokens
35.3K
Snippets
127
Records
146
Agent score
98%

What's inside Mostly Adequate Guide to Functional Programming

  1. What is an Applicative Functor?

    master

    An applicative functor is a pointed functor that provides an ap method. This interface allows you to apply functions contained within one functor to the values contained within another functor.

    Unlike Monads, which are sequential (the next step depends on the result of the previous one), Applicatives allow for independent, parallel execution. This makes them ideal for scenarios like multiple independent API calls or multiple independent DOM lookups, where you want all operations to trigger simultaneously rather than waiting for each to finish sequentially.

    Container.of(add(2)).ap(Container.of(3));
    // Container(5)
  2. What is a pure function?

    master

    A pure function is a function that satisfies two conditions:

    1. Determinism: Given the same input, it will always return the same output.
    2. No Side Effects: It does not have any observable side effects (it does not change the state of the system or interact with the outside world).

    In functional programming, pure functions are preferred over impure functions that mutate data, as they are more reliable and easier to reason about.

    const xs = [1,2,3,4,5];
    
    // pure: returns the same output per input every time
    xs.slice(0,3); // [1,2,3]
    xs.slice(0,3); // [1,2,3]
    
    // impure: mutates the original array (an observable effect)
    xs.splice(0,3); // [1,2,3]
    xs.splice(0,3); // [4,5]
    xs.splice(0,3); // []
  3. What is a Pointed Functor and the `of` method

    master

    A pointed functor is a functor that implements an of method. The of method (also known as pure, point, unit, or return) is used to place a value into a default minimal context of that type. This allows you to lift any value into the functor so you can begin using map immediately.

    Common implementations include:

    • IO.of(value)
    • Maybe.of(value)
    • Task.of(value)
    • Either.of(value) (Note: For Either, of is implemented via Right because of implies the ability to map, and Left cannot be mapped over).

    To avoid using the new keyword, it is recommended to use functor instances from libraries like folktale, ramda, or fantasy-land.

    IO.of('tetris').map(concat(' master'));
    // IO('tetris master')
    
    Maybe.of(1336).map(add(1));
    // Maybe(1337)
    
    Task.of([{ id: 2 }, { id: 3 }]).map(map(prop('id')));
    // Task([2,3])
    
    Either.of('text').map(concat('!'));
    // Right('text!')
  4. What is currying and how does it work?

    master

    Currying is a technique where you call a function with fewer arguments than it expects. Instead of returning a final value, the function returns a new function that takes the remaining arguments. This allows you to "pre-load" a function with specific arguments to create specialized versions of that function.

    By using closures, the returned function remembers the arguments passed in previous steps.

    Example: Manual Currying

    const add = x => y => x + y;
    const increment = add(1);
    const addTen = add(10);
    
    increment(2); // 3
    addTen(2); // 12
  5. What is a Natural Transformation?

    master

    A Natural Transformation is a morphism between functors—a function that operates on the containers (functors) themselves rather than the values inside them.

    Mathematically, it is a function with the signature: (Functor f, Functor g) => f a -> g a.

    A key property is that it is a structural operation (a "functorial costume change") that does not peek at the contents. For a transformation to be considered "natural," it must satisfy the naturality law: applying the transformation and then mapping a function must yield the same result as mapping the function and then applying the transformation.

    Naturality Law: compose(map(f), nt) === compose(nt, map(f))

    // nt :: (Functor f, Functor g) => f a -> g a
    compose(map(f), nt) === compose(nt, map(f));
  6. What is parametricity and how does it narrow behavior?

    master

    Parametricity is a property of polymorphic functions (functions using type variables like a) stating that the function must act on all types in a uniform manner.

    Because a function like reverse :: [a] -> [a] knows nothing about the specific type a, it cannot perform type-specific operations (like sorting numbers or comparing strings). This narrows the possible implementations to only those that rearrange the elements without inspecting or changing their underlying values. This property allows developers to reason about what a function cannot do just by looking at its signature.

  7. What is an Isomorphism?

    master

    An Isomorphism occurs when two types can be converted to one another without losing any information.

    Two types are isomorphic if you can provide both a "to" and a "from" natural transformation that, when composed, act as an identity function (returning the original value).

    Example: Promise and Task are isomorphic because you can convert a Promise to a Task and back to a Promise without changing the underlying data.

    // promiseToTask :: Promise a b -> Task a b
    const promiseToTask = x => new Task((reject, resolve) => x.then(resolve).catch(reject));
    
    // taskToPromise :: Task a b -> Promise a b
    const taskToPromise = x => new Promise((resolve, reject) => x.fork(reject, resolve));
    
    const x = Promise.resolve('ring');
    taskToPromise(promiseToTask(x)) === x;
  8. Relationship between `map` and `ap`

    master

    In an applicative functor, mapping a function f over a value x is equivalent to applying a functor containing f to a functor containing x.

    Mathematically: F.of(x).map(f) === F.of(f).ap(F.of(x)).

    This identity allows you to write code in a left-to-right fashion using ap for multiple arguments:

    Maybe.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
    F.of(x).map(f) === F.of(f).ap(F.of(x));
  9. Distinguish between Semigroups and Monoids

    master

    A Semigroup is a type that provides a binary operation concat (or similar) that is associative.

    A Monoid is a Semigroup that also provides an empty (identity) value.

    Not all Semigroups can be Monoids. For example, the First type (which always keeps the first element during concatenation) cannot have a meaningful empty value because there is no neutral element that preserves the first element of any given input.

    // First is a Semigroup but not a Monoid
    const First = x => ({ x, concat: other => First(x) })
    
    // Concatenating keeps the first ID
    Map({id: First(123), isPaid: Any(true)}).concat(Map({id: First(2242), isPaid: Any(false)}))
    // Result: Map({id: First(123), isPaid: Any(true)})
  10. Understand the Composition law for Traversables

    master

    The Composition law guarantees that swapping the order of compositions of functors does not produce unexpected results, because composition itself is a functor. This law is significant because it allows for the ability to fuse traversals, which can improve performance by reducing the number of passes over data structures.

    const comp1 = compose(sequence(Compose.of), map(Compose.of));
    const comp2 = (Fof, Gof) => compose(Compose.of, map(sequence(Gof)), sequence(Fof));
    
    // comp1(Identity(Right([true]))) === Compose(Right([Identity(true)]))
    // comp2(Either.of, Array)(Identity(Right([true]))) === Compose(Right([Identity(true)]))
  11. Implement memoization for pure functions

    master

    Pure functions can be cached by their input using a technique called memoization. This allows you to avoid re-calculating results for the same arguments by storing them in a cache.

    You can also transform impure functions (like HTTP calls) into pure ones by delaying evaluation: instead of performing the side effect immediately, return a function that performs the effect when called. The returned function is pure because it always returns the same function for the same inputs.

    const memoize = (f) => {
      const cache = {};
    
      return (...args) => {
        const argStr = JSON.stringify(args);
        cache[argStr] = cache[argStr] || f(...args);
        return cache[argStr];
      };
    };
    
    // Usage
    const squareNumber = memoize(x => x * x);
    squareNumber(4); // 16
    squareNumber(4); // 16, returns cached value
    
    // Transforming impure HTTP calls into pure functions via delayed evaluation
    const pureHttpCall = memoize((url, params) => () => $.getJSON(url, params));
  12. Understand Declarative vs Imperative Coding

    master

    In functional programming, the goal is to move from imperative coding (telling the computer how to do something via step-by-step instructions and manual state management) to declarative coding (writing a specification of what the result should be using expressions).

    Key Differences

    • Imperative: Uses loops (for, while), manual counters, and explicit array mutations (e.g., push). It focuses on the control flow.
    • Declarative: Uses higher-order functions like map and compose. It focuses on the transformation of data. This approach is often more concise, easier to optimize (via JIT), and lends itself to parallel computing because it avoids explicit order-of-evaluation dependencies.
    // imperative
    const makes = [];
    for (let i = 0; i < cars.length; i += 1) {
      makes.push(cars[i].make);
    }
    
    // declarative
    const makes = cars.map(car => car.make);