Skip to main content

Understanding Currying, Closures, and Coupling in JavaScript


Here's a problem. You have an object in your code exposed via an 
export
. You also have a function in the same file (also exported) consuming that object directly.
const info = {
    name: 'sam'
    age: 23,
    city: 'New York City'
}

function printInfo() {
    console.log(info)
} 

export {
    info,
    printInfo
}
Working in javascript, a beautiful wild west, once the object is exposed to the outside world, anything can happen. One file might import it to read its data, another might mutate it, yet another could try to restore it. Caught in the middle of this frantic struggle is your lonely function, alone, forgotten, and afraid. When inevitably invoked, the object it depends on may have changed beyond recognition causing a runtime failure.
How can we prevent such mutations and manipulations from persisting and guarantee that our function, 
printInfo
, can run with peace of mind?
The easy way out would be to wrap the object in a function and have all other references invoke the function to get a new, crisp, clean, copy. To prevent this solution, let's make the problem more contrived! We want mutations to persist across the lifecycle of our application with one exception! Our function should always consume the object in its original untouched state.
Our goals:
  • Expose the object as just an object without wrapping it in a function.
  • Ensure our function is able to use the object with a guarantee that external manipulations don't carry through via the pass by reference mechanism.
Faced with this problem at work, I looked to currying for an elegant solution.
const wrappedPrintInfo = (obj) => () => console.log(obj);

export const printInfoTwo = wrappedPrintInfo(obj)
Create a function called 
wrappedPrintInfo
 that accepts an object as its parameter and returns a function which when invoked logs that object. Then keep this wrapped version private to the file using it to generate our printInfo function which we then expose as normal.
As always, nothing works on the first try. The object is still passed by reference even through the currying and changes to 
obj
 in one part of your application will persist through 
printInfoTwo
. Unfortunately there's no getting around the pass by reference issue with just a curried function.
The curried function must make its own internal copy.
const wrapedPrintInfo = (obj) => {
    const copied = Object.assign({}, obj);

    return () => {
        console.log(copied);

    }
}

const printInfoThree = wrappedPrintInfo(obj)
Now we still use a curried function but inside the first layer we use 
Object.assign
 to create a copy of the passed in object and use this copy in the returned function. Effectively we use a closure to create a private copy of the passed in object and then return the real function in the closure.

Why Would You Ever Do This

If you don't own the exported object completely, other parts of an application may rely on it and potentially its multiple mutations. Wrapping the object in a function may lead to some unintended consequences that could break part of your application. This approach protects against those potential regressions.
Currying the function and using a closure to make a copy of the object allows for the function to only runs once when first creating the 
printInfo
 function. We also don't have to rewrite a potentially massive object in our function's scope leaving our code DRY.
Finally, since this is a curried function, should we ever need to reuse it for another object, we can do so easily with the same benefits by exposing the wrapper and currying with any other object.
The biggest benefit is a clear separation of concerns where two parts of our app remain completely untangled.

Conclusion

Doing all this to achieve a console.log is quite boring but the console log is an abstraction over what could be a far more complex set of operations. This curried function is completely agnostic to the rest of the application and completely decoupled.
Should another part of the application require the same function to run on a different object, regardless if that object is exposed, this function guarantees that the object passed in will never be modified by any other external source. It can be expanded to curry other functions, or be partially applied to other objects. Our previously timid function now stands strong and secure, guaranteeing changes elsewhere won't affect it. This isn't always the right thing to do, but demonstrates another interesting combinations of architectural principles

Comments

Popular posts from this blog

4 Ways to Communicate Across Browser Tabs in Realtime

1. Local Storage Events You might have already used LocalStorage, which is accessible across Tabs within the same application origin. But do you know that it also supports events? You can use this feature to communicate across Browser Tabs, where other Tabs will receive the event once the storage is updated. For example, let’s say in one Tab, we execute the following JavaScript code. window.localStorage.setItem("loggedIn", "true"); The other Tabs which listen to the event will receive it, as shown below. window.addEventListener('storage', (event) => { if (event.storageArea != localStorage) return; if (event.key === 'loggedIn') { // Do something with event.newValue } }); 2. Broadcast Channel API The Broadcast Channel API allows communication between Tabs, Windows, Frames, Iframes, and  Web Workers . One Tab can create and post to a channel as follows. const channel = new BroadcastChannel('app-data'); channel.postMessage(data); And oth...

Certbot SSL configuration in ubuntu

  Introduction Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free  TLS/SSL certificates , thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps. Currently, the entire process of obtaining and installing a certificate is fully automated on both Apache and Nginx. In this tutorial, you will use Certbot to obtain a free SSL certificate for Apache on Ubuntu 18.04 and set up your certificate to renew automatically. This tutorial will use a separate Apache virtual host file instead of the default configuration file.  We recommend  creating new Apache virtual host files for each domain because it helps to avoid common mistakes and maintains the default files as a fallback configuration. Prerequisites To follow this tutorial, you will need: One Ubuntu 18.04 server set up by following this  initial ...

Working with Node.js streams

  Introduction Streams are one of the major features that most Node.js applications rely on, especially when handling HTTP requests, reading/writing files, and making socket communications. Streams are very predictable since we can always expect data, error, and end events when using streams. This article will teach Node developers how to use streams to efficiently handle large amounts of data. This is a typical real-world challenge faced by Node developers when they have to deal with a large data source, and it may not be feasible to process this data all at once. This article will cover the following topics: Types of streams When to adopt Node.js streams Batching Composing streams in Node.js Transforming data with transform streams Piping streams Error handling Node.js streams Types of streams The following are four main types of streams in Node.js: Readable streams: The readable stream is responsible for reading data from a source file Writable streams: The writable stream is re...