loading...
Cover image for How To Simplifiy Code By Removing Variables

How To Simplifiy Code By Removing Variables

babak profile image Babak ・5 min read

Have you ever found yourself doing something that looks similar to this pattern?

const userInfoFromStorage = localStorage.getItem('userInfo')
const userInfoParsed = JSON.parse(userInfoFromStorage)
const userInfoClearedCache = { ...userInfoParsed, cache: {} }  
return userInfoClearedCache

Lots of use-once temporary variables. I used to do this too. I'd come up with creative ways to name those variables to sort of "self-document" what was happening. But it's generally a bad idea, here are some reasons why:

  • The more variables you create, the more likely you are to have a typo.
  • The operations performed on the data are surrounded by a lot of syntax noise.
  • Variable names don't take the place of comments.
  • Naming needless variables is an unnecessary mental load.
There are only two hard things in Computer Science: cache invalidation and naming things.

-- Phil Karlton

But what should we do instead? Clearly this isn't going to be easy code to work with:

return {
  ...( 
    JSON.parse( 
      localStorage.getItem(
        'userInfo'
      )
    )
  ),
  cache: {}
}

Yikes! 😲

While ugly, the code above does show that it is possible to express this transformation as a series of function calls with no variables. The output of one function is the input of the next function, and so on. We want to pipe values between functions. One solution is to write a function that allows us to make this abstraction. So how do we do this?

Here is one solution using reduce:

const pipeline = ( initialValue, ...fns ) => (
  fns.reduce( 
    ( val, fn ) => fn( val ), 
    initialValue 
  )
)

As you can see, this function takes an initialValue and uses reduce to feed the output of one function into the input of the next one. If you are not sure why I use reduce here, part I of my reduce tutorial may be helpful. Now we can refactor the original code like so:


return pipeline(
  // get user information stored as a string in local storage
  localStorage.getItem('userInfo'),
  // parse the string
  JSON.parse,
  // reset the cache to an empty obj
  obj => ({ ...obj, cache: {} })
)

Isn't this much cleaner? It is easy to see how data flows from top to bottom.

There are two common challenges some developers may have with this style:

  1. Types. How do you type a pipeline function?
  2. Logging (for debugging). How do you log data between the pipeline chain?

We will address these questions here:

The Typed Version

There is a way to express this function using a self-referencing type, but that is complex and warrants a separate article. For those who are curious, this is what such a type would more or less look like:

https://github.com/babakness/soultrain/blob/v0.0.43/src/type/_experiements.ts#L18

Alternatively, we can simply use TypeScript's function overloading to express how the type should evolve. For example:

function pipeline<A,B>( a: A,  ab: (a: A) => B ): B
function pipeline<A,B,C>( a: A,  ab: (a: A) => B, bc: (a: B) => C ): C
function pipeline<A,B,C,D>( a: A, ab: (a: A) => B, bc: (a: B) => C, cd: (c: C) => D):D
// and so on

This is essentially how pipeline works in my library Soultrain.

GitHub logo babakness / soultrain

Soultrain is a compact functional library written in Typescript.

Please note that this library is still experimental and there will be breaking changes. If you are kicking the tires on this within another experimental project, it is best to lock it down to a specific version.

Install

npm install soultrain

Soultrain

Soultrain is a compact functional library written in Typescript. It is inspired by Ramda and container-style programming based Algebraic Data Types.

Motivation

Ramda is a great tool, however, it relies on JS DOC comment specifications to provide type data. While this is good, Typescript provides superior static-typing. Thus this library aims to provide general purposes functional tools with much better static-typing.

Overview

This section will be completed later. From a high-level, the library provides smart curry, pipe, and pipeline functions with good type support. It currently provides two Monads, a Maybe and a Transduce. The former works similar to other Maybe libraries, however, it provides…

The benefit of the typed version is getting the compiler to double-check the code. Here is an example that the JavaScript version would not catch:

The compiler has noticed that roo is not a property on the object returned by processor.process

Logging / Debugging

To log data flowing between the functions in the pipeline, one could simply use a log function that both logs and returns the data it receives. With types it could be written as such:

const log = <A>(data: A): A => (console.log(data),data)

Or, to log with a message one could define log like this:

const noteLog = (note: unknown) => (
  <A>(data: A): A => ( console.log(note, data), data )
)

Which can be used thus:

return pipeline(
  // get user information stored as a string in local storage
  localStorage.getItem('userInfo'),
  // parse the string
  JSON.parse,
  // log the data,
  noteLog('data before cache removal'),
  // reset the cache to an empty obj
  obj => ({ ...obj, cache: {} })
)

As an exercise for the reader, one could also create a function that conditionally triggers the debugger.

Functional Programming

The pipeline function, the eager sibling of pipe and compose, will invite you to write more functions. It will also demonstrate to you how powerful currying can be; and even inform you on the order your function parameters should be.

To illustrate this, notice the arrow function in our pipeline example. Isn't obj a single-use variable? All we're using it for is as a placeholder for our data... just a few character to the right!

Turns out that by making sure our source data is the last parameter and by using curried functions, we can simplify our code even further. Below I've written how this could be expressed in vanilla JS--notice that we want to override keys for the incoming object. It will be the second parameter--thus moving from left to right, overriding the second object, obj2, with the object in the first parameter, obj1.

So instead of placing obj2 to the right of the object spread, we reverse the order and place it on the left:

const mergeLeft = curry( 
  // spread the second parameter first
  ( obj1, obj2 ) => ({ ...obj2, ...obj1 })
)

If you're unfamiliar with curry, it basically modifies our function so that it can be used as if it were also written this way:

const mergeLeft = obj1 => obj2 => ({ ...obj2, ...obj1 })

Now our solution becomes:

return pipeline(
  // get user information stored as a string in local storage
  localStorage.getItem('userInfo'),
  // parse the string
  JSON.parse,
  // log the data,
  noteLog('data before cache removal'),
  // reset the cache to an empty obj
  mergeLeft({ cache: {} })
)

Piping encourages us to write small, re-usable functions that perform specific tasks and that take the source data as the last parameter. This is similar to commands on the UNIX or Linux terminal. It's a functional programming concept that has been around for a long time. And like a great classic, it is a principle that proves itself better with time.

ES-Next

Piping data between functions is a great alternative to creating single-use variables which are only used to pass data from one function to another. Many programming languages recognize this and have an operator to perform this task. For JavaScript, there is the proposed pipeline operator

GitHub logo tc39 / proposal-pipeline-operator

A proposal for adding the simple-but-useful pipeline operator to JavaScript.

ESNext Proposal: The Pipeline Operator

This proposal introduces a new operator |> similar to F# OCaml Elixir Elm, Julia, Hack, and LiveScript, as well as UNIX pipes and Haskell's &. It's a backwards-compatible way of streamlining chained function calls in a readable, functional manner, and provides a practical alternative to extending built-in prototypes.


Warning: The details of the pipeline syntax are currently unsettled. There are two competing proposals under consideration. This readme is a minimal proposal, which covers the basic features of the pipeline operator. It functions as a strawman for comparing the tradeoffs of the competing proposals.

Those proposals are as follows:

Babel plugins for both are already underway to gather feedback.

See also the latest presentation to TC39 as well as recent GitHub issues

Which would allow us to rewrite our original example like this:

return (
 // get user information stored as a string in local storage
 localStorage.getItem('userInfo'),
  // parse the string
  |> JSON.parse
  // reset the cache to an empty obj
  |> obj => ({ ...obj, cache: {} })
) 

Until then, we can benefit from using the pipeline function today to avoid unnecessary variable declarations.

Posted on by:

babak profile

Babak

@babak

Twitter @babakness https://twitter.com/babakness

Discussion

markdown guide