DEV Community

Akshay Harti
Akshay Harti

Posted on • Edited on

Think Functionally in Javascript

Introduction

In this post, I have used a way of not using any complex jargons/terms like functors, monads, monoids, applicatives, currying etc., but explain basic concepts to start with Functional Programming (FP) sooner than you can think. I tried this approach to train my colleagues and the response was tremendously appreciative. They comprehended the concepts because it sounded familiar to their usual programming way, but with a pinch of mindset change.

First steps

I will first try to make you comfortable by not introducing FP concepts right away. Instead, I will start with what FP made me do.
It made me,

  • change my programming mindset
  • un-learn a lot of programming concepts
  • think of what to do rather than how to do
  • focus on only one thing at a time
  • reduce branching in my code
  • focus on the separation of concerns
  • think in terms of mathematical expressions (no rocket science here, it is simple)

What can we deduce from the above?

As a result, I followed 6 basic rules in my learning to develop more FP compliant code. Functions always exhibit the following properties,

  1. They are first-class citizens
  2. They obey single responsibility principle
  3. They are side-effect free
  4. They are easily testable
  5. They are declarative
  6. They are composable

According to me, the above rules are good enough for someone to get started with FP. I call them the foundations to start FP. Let's take a deep dive into each of the foundations with examples.

Functional foundations

1. Functions are first-class citizens

The simplest foundation of all. In FP, functions are everywhere, I mean, literally everywhere,

  • Functions can be variables
const myFunc = function () {
  console.log('This is a function!')
}

console.log(typeof myFunc) // function
myFunc() // This is a function!

// Arrow function way
const myArrowFunc = () => {
  console.log('This is an arrow function!')
}

console.log(typeof myArrowFunc) // function
myArrowFunc() // This is an arrow function!
Enter fullscreen mode Exit fullscreen mode
  • Functions can be passed as arguments to other functions
const sayHello = function(text) {
  return `Hello ${text}`
}

const saySomething = function(fn, text) {
  return fn(text)
}

saySomething(sayHello, "Hello World!") // Hello World!

// Arrow function way
const saySomething2 = (fn, text) => {
  return fn(text)
}
Enter fullscreen mode Exit fullscreen mode
  • Functions can be returned from other functions
const sayHello = function (text) {
      return `Hello ${text}`
}

const saySomething = function (fn) {
    return function(text) {
        return fn(text)
    }
}

const inviteSomeoneWithMessage = 
      saySomething(sayHello) // (text) => sayHello(text)

inviteSomeoneWithMessage("World!") // Hello World!

// Arrow function way
// Here, first call to saySomething2 returns a function
// (text) => fn(text)
const saySomething2 = (fn) => {
  return (text) => {
    return fn(text)
  }
}

// Arrow function shorthand
const saySomething3 = fn => text => fn(text)
Enter fullscreen mode Exit fullscreen mode

2. Functions obey single responsibility principle

In compliance with the previous foundation we need to make functions obey single responsibility principle. Now, what does that mean?

A function that obeys single responsibility principle should do one thing only. This comes from the famous SOLID principles, where, S = Single responsibility. The same thing goes for functional programming as well. Let us have a look at some examples.

Here, we have a validateFormValues function which validates the HTML form input values onSubmit.

// This function validates the form input fields - fullName and phone.
function validateFormValues(formValues) {
    const { fullName, phone } = formValues

    if(!fullName) {
        alert('Name is required')
        return false
    } else if(fullName.length <= 3) {
        alert('Name length should be greater than 3')
        return false
    }

    if(!phone) {
        alert('Phone number is required')
        return false
    } else if(phone.length === 10) {
        alert('Phone number should be of 10 characters')
        return false
    }

    return true 
}
Enter fullscreen mode Exit fullscreen mode
// Validates full name only
function isValidFullName = (fullName) => {
    if(!fullName) {
        alert('Name is required')
        return false
    } else if(fullName.length <= 3) {
        alert('Name length should be greater than 3')
        return false
    }

    return true
}

// Validates phone number only
function isValidPhone = (phone) => {
    if(!phone) {
        alert('Phone number is required')
        return false
    } else if(phone.length === 10) {
        alert('Phone number should be of 10 characters')
        return false
    }

    return true
}

// Consolidates the validations
function validateFormValues(formValues) {
    const { fullName, phone } = formValues

    if(!isValidFullName(fullName)) {
        return false
    }

    if(!isValidFullName(phone)) {
        return false
    }

    return true 
}
Enter fullscreen mode Exit fullscreen mode

There are loads of benefits of creating single-responsibility functions, which, we will eventually see in further foundations.

3. Functions are side-effect free

First let's discuss what's a side-effect? Simply stated, they are things that mutate function state outside its local environment.

let num = 0

function incrementNumber() {
   return num + 1
}

incrementNumber() // returns 1
incrementNumber() // returns 2
Enter fullscreen mode Exit fullscreen mode

The above function incrementNumber is not side-effect free because it increments a global variable. We have no way to say conclusively what the return value will be when the function is called. What does this tell us? It tells us that our incrementNumber function is not predictable. It does not return the same output for the same input. Thus, it is not a pure function.

To make a function side-effect free, we need to make it pure.

let num = 0

function pureIncrementNumber(value) {
   return value + 1
}

pureIncrementNumber(num) // returns 1
pureIncrementNumber(num) // returns 1
Enter fullscreen mode Exit fullscreen mode

The above function pureIncrementNumber is now pure because the function now increments the local value and not the global one. pureIncrementNumber has become deterministic, pure and predictable. Because it is predictable, we can easily test it, which brings us to the next foundation.

4. Functions are easily testable

Let us consider the below validation function.

let num = 0

function incrementNumber(value) {
   const data = value + 1
   console.log('data = ', data)
   return data
}

incrementNumber(num) // returns 1
incrementNumber(num) // returns 1
Enter fullscreen mode Exit fullscreen mode

Now, this may look pure but it isn't because console.log is a global function (a side-effect). So if we had to test this function,

it('should return 1 if 0 is the input') {
    const input = 0

    const output = incrementNumber(input)

    expect(output).toBe(1)
}
Enter fullscreen mode Exit fullscreen mode

The above test will pass but you will never know what was printed. If someone in your team or even yourself happened to change the console log value, you will never know because the test never asserted it. How to make sure that the test checks for the exact logged value?

let num = 0

function incrementNumber(value, logger = console.log) {
   const data = value + 1
   logger('data = ', data)
   return data
}

incrementNumber(num) // returns 1
incrementNumber(num) // returns 1
Enter fullscreen mode Exit fullscreen mode

Here, console.log is passed to the incrementNumber function as a dependency. Also, note that a default value is passed to it as well, so that it always has a value.
Now, the test would be,

it('should return 1 if 0 is the input', () => {
    const input = 0
    const consoleLog = jest.fn()

    const output = incrementNumber(input)

    expect(output).toBe(1)
    expect(consoleLog).toHaveBeenCalledWith(`data = ${input}`)
})
Enter fullscreen mode Exit fullscreen mode

I chose this kind of an example for a reason. Observe that the side-effect console.log which made the function impure has been passed as a dependency. Passing it as a dependency, has made incrementNumber a pure function.

Pure function and testability go hand-in-hand. If a function is pure then it will be easily testable.

5. Functions are declarative

Declarative comes from "Declarative Programming". What does it mean?

As per Wikipedia,

image

It means, your code should focus more on "what has to be done" rather than "how something has to be done".

Let us understand this by a simple example.

function validateLoginForm(values) {
   const { userName, password } = values
   const errors = {}
   if(!userName || userName.length === 0) {
      errors.userName = "Username is required"
   } else if(userName.length < 8) {
      errors.userName = "Username should be at least 8 characters"
   }

   if(!password || password.length === 0) {
      errors.password = "Password is required"
   } else if(password.length < 6) {
      errors.password = "Password should be at least 6 characters"
   }

   return errors
}
Enter fullscreen mode Exit fullscreen mode

The above function does a lot of things, it does not obey the single-responsibility foundation, it is not pure, because it is mutating the errors object, thus making it difficult to test. Some may feel that testing is easy but hang on, let us calculate the number of unit tests required for validateLoginForm function(we will need to test each of the branching conditions),

Code coverage No. of tests
if(!userName &#124;&#124; userName.length === 0) 4 tests
if(userName.length < 8) 2 tests
if(!password &#124;&#124; password.length === 0) 4 tests
if(password.length < 6) 2 tests

As you see, in total we have ended up writing 12 unit tests for such a simple function and that is excluding any null/undefined checks.

How can we improve this code and make it declarative so that we test it easily? The answer is to make it obey all the above foundations.

const isNull = (value) => value === null
const isTextLengthThanLessThan8 = (text) => text.length < 8
const isTextLengthThanLessThan6 = (text) => text.length < 6
const isTextEmpty = (text) => text.trim().length === 0

function validateUserName(userName = '') {
    if(isNull(userName)) {
       return "Username is required"
    }

    if(isTextEmpty(username)) {
       return "Username is required"
    }

    if(isTextLengthThanLessThan8(userName)) {
       return "Username should be at least 8 characters"
    }

    return
}

function validatePassword(password = '') {
    if(isNull(password)) {
       return "Password is required"
    }

    if(isTextEmpty(password)) {
       return "Password is required"
    }

    if(isTextLengthThanLessThan6(password)) {
       return "Password should be at least 6 characters"
    }

    return
}

function validateLoginForm(values = {}) {
    if(isNull(values)) {
       return {}
    }

    const { userName, password } = values
    return {
       userName: validateUserName(userName),
       password: validatePassword(passwrod)
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above refactored code we just write text for validateUserName and validatePassword functions because they are the only ones with branching.

Code coverage No. of tests
isNull(userName) 2 tests
isTextLengthThanLessThan8(userName) 2 tests
isNull(password) 2 tests
isTextLengthThanLessThan6(password) 2 tests

That is 8 tests. Also, we would need to write 2 tests for validateLoginForm function to check the integration. We need not test isNull, isTextLengthThanLessThan8 and isTextLengthThanLessThan6 because these are functions that do just one job and they do it well.
In total, we need 10 tests to have a sufficient code coverage.

The above validation can still be improved by one of the most powerful and important foundations, which we will see next.

6. Functions are composable

Composition/Function composition, one of the powerful foundations of FP.

As per Wikipedia,
image

Let us have a look at an example

cat result.txt | grep "composition" | tee file2.txt | wc -l
Enter fullscreen mode Exit fullscreen mode

The above Linux command uses a | (pipe) operator and is a usual way of composition in Linux.

Let us compose one of the functions isTextEmpty from the previous foundation.

const isTextEmpty = (text) => text.trim().length === 0
Enter fullscreen mode Exit fullscreen mode

Let's apply all the above mentioned foundations and break down this function.

// trim:: string => string
const trim = str => str.trim()

// size::string => int
const size = str => str.length

// isEqualToZero::int => boolean
const isEqualToZero = value => value === 0

// isTextEmpty::string => boolean
const isTextEmpty = (text) => isEqualToZero(size(trim(text)))

isTextEmpty('hello') // false
isTextEmpty('') // true
Enter fullscreen mode Exit fullscreen mode

As you can see, isTextEmpty in the above code,

  • is a composed function
  • does only one thing
  • is declarative
  • easily testable
  • side-effect free

And most importantly the function is a first-class citizen.

Tip: We can make the isTextEmpty function more declarative by using compose function from Ramda.

const isTextEmpty = compose(isEqualToZero, size, trim)
isTextEmpty('hello') // false
isTextEmpty('') // true
Enter fullscreen mode Exit fullscreen mode

You can also use another similar function flowRight from Lodash.

Let's have a look at the same validation example used in the previous foundation, but now with composition.

import { compose, isNil, isEmpty, trim, length, cond, always, T as stubTrue, filter, gt } from 'ramda'

const isTextLengthLessThan = (ltValue) => compose(gt(ltValue), length, trim)
const isEqualToZero = value => value === 0
const isTextEmpty = compose(isEqualToZero, length, trim)

const validateUserName =
    cond([
        [isNil, always("Username is required")],
        [isTextEmpty, always("Username is required")],
        [isTextLengthLessThan(8), always("Username must be minimum of 8 chars")],
        [stubTrue, always(undefined)]
    ])


const validatePassword = 
    cond([
        [isNil, always("Password is required")],
        [isTextEmpty, always("Password is required")],
        [isTextLengthLessThan(6), always("Password must be minimum of 6 chars")],
        [stubTrue, always(undefined)]
    ])

const validateValues = ({
    userName,
    password
}) =>  filter(Boolean, {
    userName: validateUserName(userName),
    password: validatePassword(password)
  })

const validateLoginForm = cond([
    [isEmpty, always({})],
    [stubTrue, validateValues]
])

validateLoginForm({}) // {} 
validateLoginForm({userName: '', password: ''})  // { userName: 'Username is required', password: 'Password is required' }
validateLoginForm({userName: 'some_username', password: 'password123'}) // {}
validateLoginForm({userName: 'user', password: 'password123'}) // { userName: 'Username must be minimum of 8 chars' }
validateLoginForm({userName: 'some_username', password: 'pass'}) // { password: 'Password must be minimum of 6 chars' }
Enter fullscreen mode Exit fullscreen mode

The above code,

  • has functions as first-class citizens
  • obeys single responsibility
    • all functions do only one thing
  • is side-effect free
    • all functions are pure
  • is easily testable
  • is declarative
    • all function names are self explanatory and sometimes comments are not needed
    • implementation is abstracted to make the code more understandable
  • is composable
    • small functions composed to form a bigger one

Notice that, there is no branching (if else statements) in the above code, thus preventing different code paths. As a result, testing of this code becomes super easy. In fact, we can only test validateLoginForm for different input variations. No tests needed for other functions because they strictly follow the foundations.

Conclusion

  • FP is not a new thing, it has been there in programming space since many years. It makes you look at a problem differently, change your existing mindset and make you unlearn a lot of programming concepts that were deeply ingrained in you.
  • FP makes your code predictable and thus makes writing unit tests easier. It makes you code with testing in mind.
  • Every foundation obeys the foundation above it.
  • FP makes you get away from imperative code.
  • The foundations I mentioned above are my way of learning FP easily and quickly. These foundations will act as stepping stones for advanced concepts of FP like immutability, functors, monads, monoids etc.,

Extra points

There are couple of things to note from the code examples,

  1. Libs like Ramda and Lodash offer a lot a utility functions that make your journey into FP easy.
  2. FP creates highly predictable code, thus leading us to focus only on the input and end result, without worring about implementation details because the logic does just one thing.

References

  1. Mastering JavaScript Functional Programming - by Federico Kereki
    https://www.oreilly.com/library/view/mastering-javascript-functional/9781839213069/

  2. https://fsharpforfunandprofit.com/video/ by Scott Wlaschin

Top comments (0)