Funtastic: a functional programming library
Recently, I created an npm package that serves as a collection of various pure functions I've written over the years. Bringing them all together into a single library made perfect sense to me, as it allows for easier reuse in other projects. While I'm aware of other similar libraries like Ramda, I wanted to create something smaller and more practical, drawing inspiration from more pragmatic languages like Rust (which, although not a functional programming language, borrows many ideas) and OCaml.
The goal of this library is to provide a lightweight, efficient, and intuitive toolset that can be easily integrated into different projects without the overhead of larger libraries. It focuses on practicality, taking cues from the simplicity of pratical languages.
Why Functional Programming?
Functional Programming (FP) offers a paradigm shift that can fundamentally transform how we approach coding, problem-solving, and software architecture. The principles of FP emphasize immutability, pure functions, and higher-order functions, which collectively foster code that is more predictable, reusable, and easier to test.
Key Benefits of Functional Programming
-
Immutability: In FP, data is immutable by default. This means once a data structure is created, it cannot be modified. This eliminates a whole class of bugs related to state changes and side effects, making your code more predictable and easier to reason about.
-
Pure Functions: Functions in FP are pure, meaning they always produce the same output given the same input and have no side effects. This leads to more reliable and testable code since functions do not depend on or alter the state of the system.
-
Higher-Order Functions: FP leverages higher-order functions, which can take other functions as arguments or return them as results. This enables more abstract and concise code, allowing for powerful patterns like function composition and currying.
-
Declarative Code: FP encourages writing declarative code, which focuses on what to do rather than how to do it. This leads to clearer and more readable code, as the intention of the code is more apparent.
-
Concurrency and Parallelism: Due to immutability and pure functions, FP naturally lends itself to concurrent and parallel programming. Without mutable state, it becomes easier to run code in parallel without running into issues related to shared state.
What is a pure function? What are side-effects?
The definition of a pure function is closely tied to the mathematical concept of a function: a mapping between two sets that follows a specific rule, where each element in the origin set (domain) is mapped to exactly one element in the destination set (codomain).
In 1936, Alonzo Church developed a formal system in mathematics known as Lambda Calculus to express any kind of computation. Lambda Calculus is Turing complete, meaning it is equivalent in power to a Turing machine. It adheres to this rule and allows for the definition of formal logical languages that operate within these constraints.
Given this definition, we can say that a pure function is a deterministic algorithm that, for a given input, always produces the same output. Computationally speaking, a pure function can be thought of as a large hash table that maps elements from the domain to the codomain. This is why computationally expensive pure functions can be memoized, and their computation can be parallelized.
When people refer to side effects, they mean that a pure function cannot access or modify any state, internal or external. Any attempt to do so compromises the purity of the function.
Funtastic
I started by defining some algebraic structures to constraint the implementations, some of them include:
- Semigroup: an algebraic structure with a binary associative operation
- Functor: a type class that represents a computational context that can be mapped over.
- Applicative: a Functor with application, allowing for functions that are also contained in a context to be applied to values in a context.
- Monad: an Applicative with a function for chaining computations.
- Future: extends the Monad interface, representing a deferred computation that can be called with arguments.
I don't want to enter in many details regarding each one of them, but if you're interested in learning more, I suggest this Reddit post.
These algebraic structures are the foundation of most of the functions that are defined in the library.
identity :: x -> x
The identity
function is a simple utility that takes a single argument and returns it without any modification. This function is often used as a placeholder or default function when a value needs to be returned unchanged.
defined :: x -> boolean
The defined
function takes a single argument and returns true
if the argument is not undefined
. It is useful for checking the presence of a value before performing operations on it, to avoid errors caused by undefined values.
declared :: this -> x -> boolean
This function evaluates the this
context where it is called. If the this
context is the declared
function itself, it defaults to using the global context (globalThis
). Otherwise, it checks if the specified property (x
) is defined on the provided context.
is :: t -> x -> boolean
Determines if a value matches a specified type, is an instance of a constructor, strictly equal to any other value or holds the same reference to an object. The function is curried, allowing partial application of arguments. It checks if the value matches the type or is an instance of the constructor function or class. The supported types include BigInt, Boolean, Function, Number, Object, Symbol, String, Functor (a.k.a has a 'map' method), Array, undefined, and null. For other types, it uses the instanceof
operator.
compose :: (...fn) -> fn
This function is a serious one, it takes a series of functions and returns a new function that, when called, applies the functions from right to left in sequence to a given argument.
Each function consumes the return value of the function that came before it. The result of the composition is a new function that takes an initial value and applies the composed functions to it in sequence.
curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
A curried function allows you to call a function with a partial set of arguments, and it will return a new function that takes the remaining arguments. This process continues until all arguments have been provided, at which point the original function is invoked with all the arguments.
map :: fn -> x -> Functor | Promise
Applies a function to each element in a functor, returning a new functor with the results. This function is curried, meaning it can be partially applied. It takes a function fn
that transforms elements of the functor x
, and returns a new functor with the transformed elements.
match :: Matchers -> x -> U
Creates a matcher function that executes a handler based on the type or value of the input.
tryCatch :: fn -> fn -> U
A higher-order function that wraps a function with try-catch block for error handling.
memoize :: fn -> T -> T
Memoizes a function to cache its results based on the provided arguments. This higher-order function returns a new function that remembers the results of previous calls with the same arguments, avoiding redundant computations and improving performance for functions with expensive calculations.
There are many other structures to mention. I will gradually update this post.