Javascript is a multi-paradigm language which can express object oriented design, imperative design, and many others, including functional programming. Namely, functions are first class objects: they can be declared, manipulated, and passed to other functions in the same way as strings, Arrays, and other primitives/objects. For www.bytethis.com, we decided to create our own library which contains common functional programming utilities to help us with working with:
- Pure Functions: functions which have no side effects and always return the same output given the same input (thus, also do not rely on global/external state).
- Immutable Objects: objects whose properties cannot be changed after creation. Anything which requires a change will return an entirely new object instead of changing the existing one.
- Declarative Operations: leveraging functions for operations instead of imperatively listing out each step of the operation.
- Memoization: caching the return value of an expensive or asynchronous function, then returning the cached value instead of re-running the function on subsequent invocations.
- Collecting Method Invocations: if two consumers call a method with the same params, we can collect the invocations and return the same result, instead of executing the method twice at the same time.
- Side-Effect Free Programming: safely performing operations without inadvertently affecting other objects or state.
There are plenty of libraries which provide these kinds of functionalities; we decided to use our own library so that we can leverage our own lightweight solution instead of using a heavier external dependency.
Funscript
Our Funscript project holds functionality for some of the most common functional patterns. The project is available as a consumable package and the code is available on Git:
- Npm Package to directly install and use.
- Github Project to view the source code.
To get started now: you can directly install via npm:
Equality Checking
Funscript provides an Equals function which will check if two items are equal to each other, including nested properties. The following are considered to be equal:
- Two primitives with the same value.
- Two objects with the same object reference.
- Two dates with the same datetime.
- Two arrays with the same contents in the same order and the contents are Equal.
- Two objects with the same keys and value pairs are Equal.
Cloning Objects
Cloning objects is useful when we want to be sure we are not causing any side effects by changing an object which is being referenced elsewhere. The Clone function will recursively clone an object's properties into a new object and return that new object. If the object has its own clone method, it will utilize that method. Otherwise, it will default to a recursive property clone.
Locking + Cloning Objects
The Lock function takes a step further: it clones an object, then locks the clone, so the resulting object cannot be mutated further (the function recursively locks the properties of the new object).
Async Array Operations
Funscript provides a FunAr object which holds functions for asynchronous versions of the array map, filter, reduce, and forEach functions. With these functions, you can avoid using for loops. Below are a few examples of the synchronous functions + their FunAr asynchronous counterparts.
Within our website's source code, reduce, then map are the two most useful async array operations. The async filter is rarely used, and the async forEach is rarely utilized, as forEach is not technically functional in the same way the others are.
Most asynchronous array functions have two versions: sequential and parallel. The sequential version will execute the callback for each item one by one, whereas the parallel version will execute the callbacks all at once.
Memoization
Memoization is a technique where a function is decorated so that return values are cached for some time, or indefinitely. The decorator handles the caching logic; if it sees a cached value for an invocation with the current arguments, it will return the cached value instead of executing the inner function again. This can be very useful for preventing expensive operations, or long running asynchronous operations, from needlessly being invoked multiple times.
With the default parameters, cached values are stored indefinitely. It is also possible to configure an expiration policy for the cache:
The examples above will work for synchronous functions, but not for async functions, as the Promises returned will be two separate objects, even if they resolve to the same value. For asynchronous memoization, we have a similar function:
The library also includes class method decorators:
Collection Pending Method Invocations
When dealing with asynchronous operations, a situation can occur where one consumer invokes the operation, then another consumer invokes it again before the first one concludes. By default, the entire method will execute again, which is undesired in many cases. To solve this, we can decorate the method / function to capture subsequent invocations, put them on old, and return the value from the first invocation to all others.
Class method decorators are also available:
Functional vs Imperative Programming
The primary purpose of this article is to discuss functional programming in Javascript and introduce Funscript, but we'd also like to give a few notes on functional vs imperative programming:
- State:
- Imperative: global / external state can exist and can be manipulated in functions, leading to side effects.
- Functional: global / external state does not exist.
- Order of Execution
- Imperative: the order of operations in the script is crucial for correct execution.
- Functional: the order is not important in most cases.
- Control Flow:
- Imperative: uses loops, conditional statements, ternary statements, and function/recursive calls.
- Functional: uses conditional statements, ternary statements, and function/recursive calls.
- Declaring Intent of Operations:
- Imperative: give step by step instructions for carrying out tasks.
- Functional: declare what information and transformations need to occur.
There are pros and cons to using different paradigms, and most large projects will use a mix between a few. We use functional programming concepts in our website but do not attempt to force it upon the entire codebase.