DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Alexander Goncharuk for This is Learning

Posted on • Updated on

Enrich your Javascript with simple functional programming techniques

Being a multi-paradigm programming language, Javascript allows writing code in different styles. And while you probably don't want to mix OOP and functional approaches on the architectural level, there is no reason to limit the power of a multi-paradigm language on a smaller code unit level. Nonetheless, functional programming techniques are often ignored where they would fit nicely. In this article, I'd like to demonstrate how one of such simple techniques can make our code better.

Without further theoretical reasoning, here is what we are going to do:

  • Write a module that checks if all object fields pass the provided validation rules.
  • Start with the most straightforward imperative implementation and improve the code in a couple of steps.
  • Compare the results.

We will not do this just to demonstrate how different techniques can be used to solve the same problem. Our goal is to improve two very important metrics of clean code: re-usability and readability. This article covers the very basics, but I will provide some links at the end if you want to dive a little deeper into this topic.

Just make it work first

Assume our application allows users to publish new articles that will show up somewhere in the feed. Just like the one you are reading now. The shape of the article will be as simple as possible:

{
  title: String;
  tags: Array<String>;
  text: String;
}
Enter fullscreen mode Exit fullscreen mode

On the server side before saving a new article in the database we have to validate if article fields conform to the following validation rules:

  • title is a string not shorter than 20 and not longer than 200 characters.
  • tags is an array of strings with at least one element in it.
  • text is a string not shorter than 200 characters and not longer than 100 000 characters.

Translated to code, the first and most naive definition of validator rules might look like this:

const rules = {
  title: value => typeof value === 'string' && value.length > 20 && value.length < 200,
  tags: value => Array.isArray(value) && value.length > 0,
  text: value => typeof value === 'string' && value.length > 200 && value < 100_000
}
Enter fullscreen mode Exit fullscreen mode

(side note) Yes, you can use the _ symbol as a numeric separator in big numbers since ES2021 to improve readability. Just in case you don't do this yet:)

Now let's create the function that will iterate over the object's fields and validate each of those. Instead of just returning true or false to indicate if the object passed validation, we want the function to return an array of erroneous fields (if any) so we can provide some feedback to the client.

const getValidationErrors = (rules, objectToValidate) => {
  return Object.entries(rules).reduce((acc, [key, validatorFn]) => {
    const value = objectToValidate[key];
    if (!validatorFn(value)) {
      return [...acc, key];
    }
    return acc;
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

The validation algorithm is quite primitive:

  • Create an array of key-value pairs of the rules object where the key is the name of the field we are validating.
  • For each key-value pair get the relevant field's value and run the validator function provided for this field name in the rules. If validation fails, add the key name in question to the errors array.
  • Return the errors array (return value of the reduce function).

We would want more from this algorithm in a real-world application(validate inputs, handle errors), but for this demo purpose let's keep the shortest version.

Obvious refactoring

There are some drawbacks to this implementation that we can spot immediately. We will likely need to validate the same conditions in other places in our application. Extracting these validators into reusable functions exported from the validation utils module is an obvious improvement.

export const isString = (value) => {
  return typeof value === 'string' || value instanceof String;
}

export const isNonEmptyArray = (value) => {
  return Array.isArray(value) && value.length > 0;
}

export const lengthIsInRange = (value, min, max) => {
  return value.length > min && value.length < max;
}
Enter fullscreen mode Exit fullscreen mode

No changes needed in the getValidationErrors function. But rules now look much better:

const rules = {
  title: isString(value) && lengthIsInRange(value, 30, 250),
  tags: isNonEmptyArray(value),
  text: isString(value) && lengthIsInRange(value, 0, 100_000)
}
Enter fullscreen mode Exit fullscreen mode

Let's look at the code we have written so far and evaluate it:

  • Our code does its job, fields are being validated.
  • The getValidationErrors function is not too bad. It's concise and provides feedback.
  • Validators are defined as reusable functions, so our code is DRY enough.

Are we good to push the changes and open a PR? πŸ‘Œ

It's probably OK to do so at this point. But our code can be made better with very little effort. We are calling functions, but do not use good functional programming practices.

Currying to make it elegant

If you look at the code you will likely notice how we repeatedly write (value) calling each validator and apply the logical AND operator multiple times. When you see this, chances are high that these repeating parts can be abstracted.

In my validator rule, I'd like to just list validators separated by commas. Then inside getValidationErrors function, I will call each of these validator functions with the value to validate as a single parameter. It's easily doable for isString and isNonEmptyArray, but lengthIsInRange has two other parameters that we need to apply in advance before the function will be called along with other validators. This is where a simple concept of currying comes into play. Let's rewrite lengthIsInRange as a higher order function:

export const lengthIsInRange = (min, max) => {
  return (value) => value.length > min && value.length < max;
}
Enter fullscreen mode Exit fullscreen mode

Here we prepare our validator by fixing min and max arguments beforehand in the lexical context of the outer function. This allows us to call the actual validator function later with only one argument: the object to validate. Which is exactly what we needed.

All we need to do the getValidationErrors function is to rename validatorFn to validators and change this line:

if (!validatorFn(value))
Enter fullscreen mode Exit fullscreen mode

to:

if (!validators.every((rule) => rule(value)))
Enter fullscreen mode Exit fullscreen mode

Here is the full version of the updated getValidationErrors:

const getValidationErrors = (rules, objectToValidate) => {
  return Object.entries(rules).reduce((acc, [key, validators]) => {
    const value = objectToValidate[key];
    if (!validators.every((rule) => rule(value))) {
      return [...acc, key];
    }
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

And our rules definitions will now look like this:

const rules = {
  title: [isString, lengthIsInRange(30, 250)],
  tags: [isNonEmptyArray],
  text: [isString, lengthIsInRange(200, 100_000)]
};
Enter fullscreen mode Exit fullscreen mode

Compare this version to previous implementations. The goal we set at the beginning was to improve code re-usability and readability. I will leave it up to you to decide which version looks better from this point of view.

Bonus: function as a language first-class object

We probably don't want to run any additional validations if the field is optional and the value for this field is missing. We hence need a way to distinguish between required and optional fields. Let's create a separate validator in its simplest possible version for this purpose:

export const isDefined = (value) => {
  return value !== undefined;
}
Enter fullscreen mode Exit fullscreen mode

And update our rules, making tags field optional:

const rules = {
  title: [isDefined, isString, lengthIsInRange(30, 250)],
  tags: [isNonEmptyArray],
  text: [isDefined, isString, lengthIsInRange(200, 100_000)]
};
Enter fullscreen mode Exit fullscreen mode

Now in our getValidationErrors function, we can check if isDefined is among validators first and only then run the remaining validations, otherwise just proceed to the next field. How do I do this? As you know, functions are first class objects in Javascript, so I can check for function presence in an Array just like I would do for a string or a number:

if (!isDefined(value) && !validators.includes(isDefined)) {
  return acc;
}
Enter fullscreen mode Exit fullscreen mode

Full version of revamped getValidationErrors:

const getValidationErrors = (rules, objectToValidate) => {
  return Object.entries(rules).reduce((acc, [key, validators]) => {
    const value = objectToValidate[key];
    if (!isDefined(value) && !validators.includes(isDefined)) {
      return acc;
    }
    if (!validators.every((rule) => rule(value))) {
      return [...acc, key];
    }
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

If the value is not defined and isDefined is not among validators the function returns early. Since it happens in the reduce context, don't forget to return the unchanged accumulator value. This way, we keep our rules definitions clean, avoiding the need of creating data structures like {required: true, validators: [...]}. πŸ‘

Bear in mind this is not necessarily the way to go for every single task. You should always weigh the pros and cons and pick the right data structure for your requirements. But to make efficient choices it is paramount to be aware of the capabilities that language offers you.

Is getValidationErrors getting too messy?

It probably is. While the function body is still only 9 lines long and its readability is acceptable, the function is doing several things now. Since we are talking about functional programming techniques, let's turn getValidationErrors into a chain of operations applied to the rules object entries:

const getValidationErrors = (rules, objectToValidate) => {
  return Object.entries(rules)
    .map(([field, validators]) => ({
      field,
      validators,
      value: objectToValidate[field]
    }))
    .filter(shouldReportValidationError)
    .map(({field}) => field);
}

const shouldReportValidationError = (validationData) => {
  return shouldBeValidated(validationData) && !passesAllValidators(validationData);
}

const shouldBeValidated = ({validators, value}) => {
  const canSkipValidation = !isDefined(value) && !validators.includes(isDefined);
  return !canSkipValidation;
};

const passesAllValidators = ({validators, value}) => {
  return validators.every((rule) => rule(value));
};
Enter fullscreen mode Exit fullscreen mode

A quick look at the getValidationErrors is enough now to understand it is doing the following things for every field:

  • Mapping the field's validation rules and value to a data structure for further processing.
  • Applying validation rules abstracted in shouldReportValidationError.
  • Mapping erroneous fields to the correct error format (just the name of the field in our case).

In a real-world scenario when the logic behind every step will likely be more complex, the value of such refactoring grows. We decomposed the logic in several steps and no longer mix array processing with imperative code on the getValidationErrors level.

Code examples

You can play around with code examples from this article in an online editor by this link. Just type node <file name>.js in the terminal to run the code.

Conclusion

Hope this article motivates you to leverage currying and other useful functional programming techniques in your Javascript code.

If you wish to learn more about functional techniques in Javascript I highly recommend this series of articles by Randy Coulman. In particular, the third article in the series explains the idea of currying or partial application in more detail.

Thanks for reading!

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.