Skip to content

Latest commit

 

History

History
1220 lines (947 loc) · 33.5 KB

3-functions.md

File metadata and controls

1220 lines (947 loc) · 33.5 KB

Functions

1. Use default arguments instead of short circuiting or conditionals

Be aware that if you use them, your function will only provide default values for undefined arguments. Other falsy values will not be replaced as it happens while short circuiting

// Bad! But was a necessity before ES2015...
function setName(name) {
    const newName = name || 'Juan Palomo';
}

// Good! All the goodness of ES2015+
function setName(name = 'Juan Palomo') {
    // ...
}
// Good! An example of destructuring + default values
function test({name} = {}) {
  console.log (name || 'unknown');
}

2. Reduce function arguments (2 or fewer ideally)

  1. An adequate number could be 2 or less, but don't obsess over it
  2. If you need to increase the arguments, use an object as an "options" parameter to group together multiple arguments.
  3. Using an object to grouped parameters has an added benefit in the sense that it creates a higher-level abstraction, closer to business logic

Note:

Three arguments should be avoided if possible (since they take a lot of time and brainpower to understand). Anything more than that should be consolidated!

Solution:

Create an object and pass it to the function. Use destructuring to pull out arguments in a similar fashion to multiple argumtns.

// Bad!
function newBurger(name, price, ingredients, vegan) {
  // ...
}

// Good!
function newBurger(burger) {
  // ...
}

// Even better! Destructuring
function newBurger({ name, price, ingredients, vegan }) {
  // ...
} 

// Usage becomes easier - abstraction:
const burger = {
  name: 'Chicken',
  price: 1.25,
  ingredients: ['chicken'],
  vegan: false,
};
newBurger(burger);

Alternative

If the arguments are all of the same type, consider using the rest operator to clubs all of them into a single array.

For example, a list of arguments that are number to be added can be condensed using the rest operator.

// Bad!
function add() {
  return [...arguments].reduce((a, b) => a + b, 0);
}

// Good!
function add(...args) {
  return args.reduce((a, b) => a + b, 0);
}

3. Function names should say what they do

// Bad!
function addToDate(date, month) {
  // ...
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);

// Good!
function addMonthToDate(month, date) {
  // ...
}
const date = new Date();
addMonthToDate(1, date); // More readable

Use verbs and keywords:

A good example is copyArray which means that it is a function that copies an array.

Verb: A word used to describe an action, state, or occurrence, and forming the main part of the predicate of a sentence, such as hear, become, happen.

Verbs are "doing" words. Verbs can express physical actions (e.g., play, dive), mental actions (e.g., think, guess), or states of being (e.g., exist, am).

Longer names are OK

Longer names are needed to convey all the information that we need to know. Follow the javascript convention - camelCase for variables, functions, etc and PascalCase for constructor functions and classes

4. Avoid side effects - Global Variables

Anything that is outside the scope of the function that is affected or utilized by it is a side-effect. Pure functions depend on input only - they don't have side-effects.

Chances of having errors in our code grows heavily as we include more side effects. However, we cannot have a side-effect free application! There will be side effects in our app because if it affects something in the enivroment then that's its purpose and also a side-effect.

Example of a side effect: Code in which a function modifies a variable or object that is outside its scope. This function cannot be tested because it has no arguments to test.

Our aim should be to reduce the side-effects, especially unintended ones.

Caution:

Avoid side effects at all costs to be able to generate functions that can be tested, apply techniques such as memoization and other advantages

Solution:

The easiest way to avoid this side effect is passing the variables that are within the scope of this function (Something obvious but not so obvious when we have to have it aimed to remember it over time) as an argument.

// Bad!
let fruits = 'Banana Apple';
function splitFruits() {
  fruits = fruits.split(' ');
}
splitFruits();
console.log(fruits); // ['Banana', 'Apple'];

// --------------------

// Good! Send arguments
function splitFruits(fruits) {
  return fruits.split(' ');
}
const fruits = 'Banana Apple';
const newFruits = splitFruits(fruits);
console.log(fruits); // 'Banana Apple';
console.log(newFruits); // ['Banana', 'Apple'];

5. Avoid side effects - Objects Mutables

Similar to global variable changes, but when dealing with objects, be careful not to mutate them. This can occur even if they are passed as arguments and not accessed via scope

// Bad! `cart` is mutated
const addItemToCart = (cart, item) => {
  cart.push({ item, date: Date.now() });
}; 

// Good! Avoiding mutation, keeping function pure...
const addItemToCart = (cart, item) => {
  return [
    ...cart,
    {
      item, 
      date: Date.now(),
    }
  ];
};

6. Functions should do one thing

Each function must do only one conceptual task. A set of small tasks together will make a larger task but the tasks should not be intermingled, this is known as coupling.

Thumb rule to think about splitting a function:

You should fear when you write an "if" in your code. It can be a reason to create a separate function of the conditional itself

// Bad: We are filtering active customers, sending email, etc:
function emailCustomers(customers) {
  customers.forEach((customer) => {
    const customerRecord = database.find(customer);
    if (customerRecord.isActive()) {
      email(customer);
    }
  });
}

// ------------------

// Good! Separation of concerns
function isActiveCustomer(customer) {
  const customerRecord = database.find(customer);
  return customerRecord.isActive();
}
function emailActiveCustomers(customers) {
  customers
    .filter(isActiveCustomer)
    .forEach(email);
}

7. Functions should only be one level of abstraction

Each function should only have a single level of abstraction.

Solution:

Identify the different levels of abstraction and create functions that meet the requirements using the other clean coding techniques for functions listed above

When you have more than one level of abstraction your function is usually doing too much. Splitting up functions leads to reusability and easier testing.

// Bad! tokenizer, Lexer, parser, .. all together
function parseBetterJSAlternative(code) {
  const REGEXES = [
    // ...
  ];
  const statements = code.split(' ');
  const tokens = [];
  REGEXES.forEach((REGEX) => {
    statements.forEach((statement) => {
      // ...
    });
  });
  const ast = [];
  tokens.forEach((token) => {
    // lex...
  });
  ast.forEach((node) => {
    // parse...
  });
}    

// --------------------

// Good!
const REGEXES = [ // ...];
function tokenize(code) {    
  const statements = code.split(' ');
  const tokens = [];
  REGEXES.forEach((REGEX) => {
    statements.forEach((statement) => {
      tokens.push( /* ... */ );
    });
  });
  return tokens;
}
function lexer(tokens) {
  const ast = [];
  tokens.forEach((token) => ast.push( /* */ ));
  return ast;
}
function parseBetterJSAlternative(code) {
  const tokens = tokenize(code);
  const ast = lexer(tokens);
  ast.forEach((node) => // parse...);
}

Another example:

Each function should only have one level of abstraction. This means if a function does something that has a high level of abstraction then it should only do that.

For example, if we want to write a function that loops through elements of an array and adds it to a list, then it should only do that.

Below is an example of dividing code into functions by the level of abstraction:

// Good!
const addFruitLis = (fruits, ul) => {
  for (const f of fruits) {
    const li = document.createElement('li');
    li.innerHTML = f;
    ul.appendChild(li);
  };
}
const addFruitUl = (fruits) => {
  const ul = document.createElement('ul');
  addFruitLis(fruits, ul);
  document.body.appendChild(ul);  
}
const fruits = ['apple', 'orange', 'grape'];
addFruitUl(fruits);

In the code above, we have a function addFruitLis that create the li elements and append it to the ul element that’s in the parameter.

This is one level of abstraction because we’re adding the li elements after the ul element is generated. It’s one level below the ul in terms of hierarchy.

Then we defined the addFruitUl function to create the ul element and delegate the addition of li elements to the addFruitLis function. Then the ul is appended to the document's body. This way, each function only does as little as possible.

Each function only deals with one level of abstraction, as addFruitLis only deals with the li elements in the ul element and addFruitUl only deals with the ul element.

The wrong way to write the code above would be to combine everything into one function. It makes the function’s code complex and confusing.

8. Favor functional programming over imperative programming

Advantages:

  1. Pure functions are easier to reason about
  2. Testing is easier, and pure functions lend themselves well to techniques like property-based testing
  3. Debugging is easier
  4. Programs are more bulletproof
  5. Programs are written at a higher level, and are therefore easier to comprehend
  6. Function signatures are more meaningful
  7. Parallel/concurrent programming is easier

Another feature of functional programming versus imperative programming is that the code is more readable

const items = [
  {
    name: 'Coffee',
    price: 500
  }, {
    name: 'Ham',
    price: 1500
  }, {
    name: 'Bread',
    price: 150
  }, {
    name: 'Donuts',
    price: 1000
  }
];
// Bad! Harder to read...
let total = 0;
for (let i = 0; i < items.length; i++) {
  total += items[i].price;
}

// Good! Easy to understand
const total = items
  .map(({ price }) => price)
  .reduce((total, price) => total + price);

9. Remove duplicate code

Do your absolute best to avoid duplicate code. Duplicate code is bad because it means that there's more than one place to alter something if you need to change some logic.

Oftentimes you have duplicate code because you have two or more slightly different things, that share a lot in common, but their differences force you to have two or more separate functions that do much of the same things. Removing duplicate code means creating an abstraction that can handle this set of different things with just one function/module/class.

Note:

Getting the abstraction right is critical, that's why you should follow the SOLID principles laid out in the Classes section. Bad abstractions can be worse than duplicate code, so be careful!

// Bad!
function showDeveloperList(developers) {
  developers.forEach(developer => {
    const expectedSalary = developer.calculateExpectedSalary();
    const experience = developer.getExperience();
    const githubLink = developer.getGithubLink();
    const data = {
      expectedSalary,
      experience,
      githubLink
    };

    render(data);
  });
}

function showManagerList(managers) {
  managers.forEach(manager => {
    const expectedSalary = manager.calculateExpectedSalary();
    const experience = manager.getExperience();
    const portfolio = manager.getMBAProjects();
    const data = {
      expectedSalary,
      experience,
      portfolio
    };

    render(data);
  });
}
// Good!
function showEmployeeList(employees) {
  employees.forEach(employee => {
    const expectedSalary = employee.calculateExpectedSalary();
    const experience = employee.getExperience();

    const data = {
      expectedSalary,
      experience
    };

    switch (employee.type) {
      case "manager":
        data.portfolio = employee.getMBAProjects();
        break;
      case "developer":
        data.githubLink = employee.getGithubLink();
        break;
    }

    render(data);
  });
}

10. Set default objects inside functions with Object.assign

Instead of using short-circuiting to enable defaults for properties, use Object.assign()

// Bad!
const menuConfig = {
  title: null,
  body: "Bar",
  buttonText: null,
  cancellable: true
};

function createMenu(config) {
  config.title = config.title || "Foo";
  config.body = config.body || "Bar";
  config.buttonText = config.buttonText || "Baz";
  config.cancellable =
    config.cancellable !== undefined ? config.cancellable : true;
}

createMenu(menuConfig);
// Good!
const menuConfig = {
  title: "Order",
  // User did not include 'body' key
  buttonText: "Send",
  cancellable: true
};

function createMenu(config) {
  config = Object.assign(
    {
      title: "Foo",
      body: "Bar",
      buttonText: "Baz",
      cancellable: true
    },
    config
  );

  // config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
  // ...
}

createMenu(menuConfig);

11. Use blocks in ES6 instead of IIFEs to create namespaced code

With ES6, we can create block scoped variables using let and const

// Bad!
(function() {
  var numFruits = 1;
})();

// Good!
{
  let numFruits = 1;
};

Even better is to use modules (separate files) and code there, exporting only what you need:

// Good!
// File: redColor.js
// Some side effect
export color = 'red'; // Main export

// File: someOtherModule.js
import { color } from './fruit'
console.log(color);

12. Returning early as a way to remove conditional complexity

Returning is not only useful for shifting control from one function to another. By returning early we are using its side-effect which is avoiding work that exists on lines below itself

// Bad!
function isDancer(person) {
  if (person) {
    if (person.hasMoves && person.hasMoves.length) {
      if (person.hasMoves.type === 'DANCE') {
        return true
      } else {
        return false
      }
    }
  } else {
    return false;
  }
}

By employing pre-emptive input checks, we are reducing the complexity and the congitive burden on the user. Hence, readabilty is improved too!

// Good!
function isDancer(person) {
  if (!person || !(person.hasMoves && person.hasMoves.length)) {
    return false
  }

  if (person.hasMoves.type === 'DANCE') {
    return true
  }

  return false
}
// Even better!
function isDancer(person) {
  if (hasNoValidMoves(person)) {
    return false
  }

  if (isMoveTypeDance(person.hasMoves.type)) {
    return true
  }

  return false
}

function hasNoValidMoves(person) {
  const { hasMoves } = person || {}
  return !(hasMoves && hasMoves.length)
}

function isMoveTypeDance(type) {
  const DANCE_MOVE_TYPE = 'DANCE'
  return type === DANCE_MOVE_TYPE
}

13. If dealing primarily with switch cases, return directly from each case

It reduces the cognitive load on the user by removing the need to process what is below the switch case or below the switch construct itself. Improves readability.

// Good!
function generateWelcomeMessage(language) {
  let message;

  switch (language) {
    case 'DE':
      message = 'Willkommen!';
    case 'FR':
      message = 'Bienvenue!';
    default:
      message = 'Welcome!';
  }

  return message;
}
// Good!
function generateWelcomeMessage(language) {
  switch (language) {
    case 'DE':
      return 'Willkommen!';
    case 'FR':
      return 'Bienvenue!';
    default:
      return 'Welcome!';
  }
}

14. Do not invoke functions using bind(), call(), or apply unless dealing with low-level code

The most readable, simple and easily understood method of invocation is the humble parentheses () which does not intentionally manipulate the this value. Ex: someFunction()

Use cases for bind(), apply(), or call():

  • Low level libraries consumed by others (Ex: DOM manipulation, logging tools, etc - you decide)
  • Utility functions that are used by other higher order code (such as react component code)

One reason why usage of bind() is frowned upon in React class components

There are many reason but the foundation for all those reasons is that bind() is a low level implementation specific code. React components, however, are already meant to be high level - with the low level aspects of it such as the reconciliation, DOM manipulation, etc already abstracted by react and react-dom libraries themselves. So, why should I, as consumer of react, deal with the low level complexities? (Note: Hooks solve this problem)

A. Partial applications are a good reason to use them

A great use case for bind is Partial Applications of functions. (i.e. giving a function only some of it’s arguments).

// Good!
function list(...args) {
  return [...args]
}

function addArguments(arg1, arg2) {
  return arg1 + arg2
}

const list1 = list(1, 2, 3);
//  [1, 2, 3]

const result1 = addArguments(1, 2);
//  3

// Create a function with a preset leading argument
const leadingThirtysevenList = list.bind(null, 37);

// Create a function with a preset first argument.
const addThirtySeven = addArguments.bind(null, 37); 

const list2 = leadingThirtysevenList(); 
//  [37]

const list3 = leadingThirtysevenList(1, 2, 3); 
//  [37, 1, 2, 3]

const result2 = addThirtySeven(5); 
//  37 + 5 = 42 

const result3 = addThirtySeven(5, 10);
//  37 + 5 = 42 
//  (the second argument is ignored)

A. Async stuff when you want to control this when function is invoked is another use-case

For example, in React we bind our async callbacks that are sent to events as callback (Ex: Form onSubmit event). As much as it is frowned upon from a design perspective, for the component to work as expected, it is a necessity to bind the function that will be executed async. Therefore, apart from the debates on the library's design, for a consumer of it, it is a perfectly valid scenario to use bind for this purpose

15. Function declarations vs Expressions vs Arrow Functions

There are many ways to declare a function. The following tips can help you decide which one to use.

  1. Use arrow function for traditional lambda methods (Pure functions)

For callbacks or handlers not manipulating this, use arrow functions. A good example would be inside functional programming style methods such as array built-ins: filter, map, reduce, etc.

// Good!
const hasId = item => item.id !== undefined
const renderItem = ({ name, id}) => `${id}: ${name}`

function getMarkupForItemsWithIds(items) {
  return items
    .filter(hasId)
    .map(renderItem)
}

The above code is clean because the readability has increased with arrow functions. It is a very succinct syntax.

  1. Use arrow function for promise chains

It is a callback and does not get invoked with any particular this. Therefore, it is the perfect place to use arrow functions.

The reasons are same: Readability and succinctness. If we receive a state and return is a manipulation of that, arrow functions make it even more readable

// Good!
someAPIPromise
  .then(response => dispatch({
    type: API_RESPONSE,
    payload: response.data
  }))
  .catch(error => {
    throw new ApiError(error)
  })
  1. Use arrow function for object transformations
// Good!
export default {
  computed: {
    ...mapState({
      results: state => state.results,
      users: state => state.users,
    });
  }
}
  1. Use function expressions where this is dynamic (a.k.a Do not use arrow functions)

This is usually applied to class methods such as React class components.

Limitations of arrow functions:

  • It does not provide access to bindings such as this or arguments
// Bad! Will give unexpected results
class Counter {
  // Using the class-properties proposal 
  // Not valid syntax without the babel plugin for it:
  counter = 0;

  handleClick = () => {
    this.counter++;
  }

  constructor() {
    // Binding is useless!! The following line does not work!
    this.handleClick = this.handleClick.bind(this);
  }
}
// Good!
class Counter {
  counter = 0;

  handleClick() {
    this.counter++;
  }

  constructor() {
    this.handleClick = this.handleClick.bind(this);
  }
}
  • It does not have a prototype property, so it cannot be used as a constructor. Therefore, it is not a good idea to use it as a class, object, or prototype method!
class FooBear {
  name = 'Foo Bear';
}
// Bad!
FooBear.prototype.sayHello = () => `Hello I am ${this.name}`

new FooBear().sayHello() // "Hello I am "
// Good
FooBear.prototype.sayHello = function() {
  return `Hello I am ${this.name}`;
};
new FooBear().sayHello() // "Hello I am Foo Bear" 

// Same logic applies to object and class methods!

An object method example:

// Bad!
const calculate = {  
  array: [1, 2, 3],
  sum: () => {
    console.log(this === window); // => true
    return this.array.reduce((result, item) => result + item);
  }
};
console.log(this === window); // => true
// Throws "TypeError: Cannot read property 'reduce' of undefined"
calculate.sum();
// Good!
const calculate = {  
  array: [1, 2, 3],
  sum: function() { // Or `sum() {` which is the same thing!
    console.log(this === window); // => false
    return this.array.reduce((result, item) => result + item);
  }
};
console.log(this === window); // => true
calculate.sum(); // 6

A constructor example:

// Bad!
const MyCat = name => {
  this.catName = name;
}

new MyCat('Alex').catName // Uncaught TypeError: MyCat is not a constructor
// Good!
// In the case of constructors, it is more common to use a fn. declaration!
function MyCat(name) {
  this.catName = name;
}

new MyCat('Alex').catName // Alex
  1. Do not rely on hoisting when using function declarations

Hoisting is not intuitive. As a JS developer, we are expected to know the concept but while reading through code, it takes more mental effort to map a function declared somewhere downstream in the scope to its usage much earlier in the same scope.

// Bad!
someFunction()

// ... 
// other things
// ...

function someFunction() {}
// Good!
function someFunction() {}

// ... 
// other things
// ...

someFunction()
  1. Caution: Do not let arrow functions obscure the code too much

Arrow functions make everything succinct but too much of it can lead to loss of readability. Only the right amount makes it readable.

// Bad!
const multiply = (a, b) => b === undefined ? b => a * b : a * b;
const double = multiply(2);
double(3);      // => 6
multiply(2, 3); // => 6
// Good!
// Using arrow functions but with clarity:
const multiply = (a, b) => {
  if (b === undefined) {
    return b => a * b
  }
  
  return a * b;
}
const double = multiply(2);
double(3);      // => 6
multiply(2, 3); // => 6

16. Cleaner parameters with default args, destructuring, and rest params

The usage of the 3 techniques gives us very predictable code

Default arguments

  • They help give placeholder values whenever the actual value is missing
  • The placeholder value gives a hint on what data type is expected
// Bad!
function convertCurrency = (value, conversionRate) => {
  conversionRate = conversionRate || 1
  return value * conversionRate
}
convertCurrency(100) // 100
// Good!
function convertCurrency = (value, conversionRate = 1) => {
  return value * conversionRate
}
convertCurrency(100) // 100

Destructuring

  • Destructuring can happen inside function arguments' list or outside it
    • If it happens inside arguments, it is better for readability & preditable code
  • Can use a single object param instead of an ever-growing list of arguments (order does not matter)
  • Can assign key to variable of the same name
  • Can also assign key to variable of a different name
  • Can destructure arrays as well (but the order matters)
// Bad!
function saveCityAndState(location) {
  const latitude = location.latitude
  const longitude = location.longitude
  // ...
}
// Good!
function saveCityAndState({ latitude, longitude }) {
  // ...
}
// Good!
// Default values (as Bengaluru coordinates) within a destructure
function saveCityAndState({ latitude = 12.97, longitude = 77.59 }) {
  // ...
}
// Good!
// Renaming destructured values
function saveCityAndState({ lat: latitude, long: longitude }) {
  long // Error - not available
  longitude // Accessible ☑
  // ...
}

The major benefit of objects as parameters is that we can have extensible functions:

// Bad!
function foo(param1, param2, param3) {

}

// If 2nd argument is not passed, it needs to 
// have undefined as placeholder
foo(param1, undefined, param3)
// Good!
function foo(param1, options) {
  const { param2, param3 } = options
}

Rest parameters

  • It is useful when we have an unknown number of similar arguments
  • We do not have to force consumer of code to pass in an array!
  • It is a great way to pass props if you do not plan on altering them (If you do, use objects and the destructuring approach)

The rest operator has to be the last argument!

// Bad!
// Cannot even extend the function without modifying it
function validateCharacterCount(max, item1, item2, item3) {
  if (item1.length > max) { return false }
  if (item2.length > max) { return false }
  if (item2.length > max) { return false }
  return true
}
validateCharacterCount(5, "sugar", "watermelon", "fig", /* Not even considered -> */ "apple")

// Also bad but better!
// Forcing consumer to send an array of similar params
// instead of appending them to the list
function validateCharacterCount(max, itemsList) {
  return itemsList.every(item => item.length < max)
}
validateCharacterCount(5, ["sugar", "watermelon", "fig"])
// Good!
function validateCharacterCount(max, ...items) {
  return items.every(item => item.length < max)
}
validateCharacterCount(5, "sugar", "watermelon", "fig")
// If consumer has a list already, they can "spread" it out:
const fruitList = ["mango", "watermelon", "fig"]
validateCharacterCount(5, ...fruitList)

Note:

We can use rest parameters inside object and array destructuring to get the remaining key/values as a separate object:

// Bad!
function savePhotoLocationData(photo) {}
// Good!
function savePhotoLocationData({ 
  coordinates,
  city,
  state,
  ...additionalData
}) {
  // Additional data can be used if required
  // It will not contain coordinates, city, state
}

17. Flexible functions: Testable function using dependency injection

Coupling is the degree of interdependence between software modules

If a function directly depends on another piece of data or functionality outside of its scope, it is said to have a tighter coupling with that piece. It is bad most of the time because it reduces flexibility and reusability of code, makes changes much more difficult,impedes testability etc.

Tightly coupled functions are bad! One of the main reasons is that it is hard to test such functions. Testing is important - it makes code easy to refactor.

Dependency injection: Instead of tightly coupling the dependency with the function, it is passed as an argument! In this way, we don't have to mock or stub the dependency but pass it into the function like any other value

Too many stubs and mocks? Although it is inevitable to have some mocks/stubs in code, too many of them is a code smell! It is a clue that your code is tightly coupled and complex. We must simplify it (& dependency injection is one of the ways)

// Bad! Tightly coupled
// "./currency-converter.js"

import { fetchConversionRate } from './utils'

function currencyConverter(amount, from, to) {
  // ...
  const convertedAmount = amount * fetchConversionRate(from, to) // Tight coupling
  // ...
  return convertedAmount
}

currencyConverter(100, '$', 'Rs')
// Testing "currencyConverter":
// ./currency-converter.test.js

import currencyConverter from './currency-converter'
import utils from './utils' // Have to also import the dependency

it("Should return 0 if conversion rate is 0", () => {
  const fetchConversionRate = jest.fn(utils, "fetchConversionRate")
  expect(currencyConverter(100, '$', 'Rs')).toBe(0)
})

By injecting the dependency, we get better code! Easily testable code.

// Good!
import { fetchConversionRate } from './utils'

function currencyConverter(amount, { from, to, converter }) {
  // ...
  const convertedAmount = amount * fetchConversionRate(from, to) // Tight coupling
  // ...
  return convertedAmount
}

currencyConverter(100, { 
  from: '$', 
  to: 'Rs',
  converter: fetchConversionRate
})
// Testing "currencyConverter"
// ./currency-converter.test.js

import currencyConverter from './currency-converter'
// Importing the dependency from "utils" is not necessary

it("Should return 0 if conversion rate is 0", () => {
  const zeroReturningConverter = () => 0
  expect(currencyConverter(100, {
    from: '$', 
    to: 'Rs',
    converter: zeroReturningConverter
  })).toBe(0)
})

18. Flexible functions: Single responsibility (partial application & currying with array methods)

Functions must have a single responsibilty and they must be of a single level of abstraction. Partial application and currying help with it

Both partial application and currying are higher-order functions i.e functions returning other functions so they need to be called multiple times before they are fully resolved.

Higher order functions are good for writing testable code and moreover, reusable code! They are reusable because we start off with the baseline requirements for a function, and for every new function it returns, we add more requirements. Therefore, it starts of with a generic function which is built upon by more specific functions

The following are the ways to break up functions of higher arity and supply arguments as and when needed instead of knowing everything beforehand

Partial application: A partially applied function reduces the total number of arguments for a function (known as "arity") while giving you back a function that takes in fewer arguments.

We mostly use arrow functions for readability i.e to remove extraneous information

// Bad!
function createEvent(location, owner, date, time) {
  return {
    ...location,
    owner,
    date,
    time
  }
}

createEvent("Bengaluru", "Brad", "10-07-20", "4pm")
createEvent("Bengaluru", "Bob", "10-11-20", "10am")

// Two problems:
// 1. Function needs to know a lot of things: location, owner, & time
// 2. Repetition

We can create a higher order function using partial application. Our baseline function can be related to the location

// Good!
function createLocationSpecificEvent(location, owner) {
  return (date, time) => ({
    ...location,
    owner,
    date,
    time
  })
}

const createBengaluruEvent = createLocationSpecificEvent("Bengaluru", "Brad")
createBengaluruEvent("10-07-20", "4pm")
createBengaluruEvent("10-11-20", "10am")

// Solved one problem:
// 1. Reusable, SRP function

// A problem that still persists:
// 1. Repetition (of createBengaluruEvent) - not exactly reusable! What if owner changed?

Currying: Currying is similar to partial application but instead of taking partial arguments and returning a function, it takes exactly one argument and returns a series of functions that each return a function taking in one argument until it is fully resolved. Hence, arity is always 1

Why currying?

A higher-order functions with hardcoded parameters are less flexible than a function that takes in one argument at a time and partially applies it serially!

Currying helps avoid repetition.

// Good!
const createEvent = location => owner => date => time => ({
  ...location,
  owner,
  date,
  time
})

const bangaloreEvent = createEvent("Bangalore")
const createBradEvent = bangaloreEvent("Brad")
eventsUnderBrad("10-07-20")("4pm")

const mumbaiEvent = createEvent("mumbai") // similar reusability

// Super testable!
// No repetition - completely reusable

Currying works really well with functions that use array methods for the return value (We pass in the callback to the one of the functions)

// Bad!
const filterUsers = (users, field, value) => {
  return users.filter(user => user[field] === value)
}

// A highly restrictive function!
// Need to know everything before hand
// Adds to repetition
// Good!
const filterListOfObjects = field => value => users => users.fitler(user => user[field === value])

const matchByAge = filterListOfObjects("age")
const matchAboveAge18 = matchByAge(18)
const usersAboveAge18 = matchAboveAge18(users)

Repetition is a clue that a natural division exists within the function and that we can break it down further

19. Flexible functions: Avoid context (this) confusion with arrow functions

  • When the context is not important (i.e it must not change, lexically speaking) we must use arrow functions
  • However, if we are writing classes and the context can be dynamic (such as react class methods) then it is imperative to preserve the context. In this case, do not use arrow functions. Use function expressions instead

Reasons to use arrow functions heavily:

  • Readability
  • Predictability (always know what is returned)
  • Great for composing higher order functions: partial applications and currying