Functions
A good function typically takes one or only a few parameters and returns an output. If you’re writing functions that need many many parameters, it might be a sign to split it up into smaller functions. Likewise if you can’t fit the code for the function on a single page of your editor: it probably ought to be made more digestable.
Two of the common ways you’ll see a function expressed in Javascript:
function blah() {...}
const blah = () => { ...
}
The first (somewhat old-school) style has the occasional benefit that you can call a function before it shows up in the source code.
// This case worksblah();function blah(blah) { ... }
// This case does not workblah();const blah = () => { ... }
The demos mostly prefer the second form, the so-called ‘fat arrow’ syntax. One reason is that it’s more succinct, and when the body of the function is a simple expression, you can do away with the { }
and return
as well:
const doubleValue = v => v*2;// Same as these twoconst doubleValue = (v) => { return v*2 }function doubleValue(v) { return v*2 }
When using functions as parameters to other functions, the preferred syntax is less cluttered, allowing you to see what’s going on more clearly.
// So smooth....someArray.filter(v => v.startsWith(`a`));
// So harsh...someArray.filter(function(v) { return v.startsWith(`a`);})
There are some other advantages and disadvantages of the two function styles, but for the context of the ixfx demos, these don’t matter.
Parameters
When making a function, it probably ought to be taking some input.
const greet = (name) => `Hello ${name}!`;greet(`John`); // `Hello John!`
Using type annotations, we can also suggest what kind of data the parameter should be. This helps when calling and writing the function, because your code editor will give you helpful warnings.
/** * @param {string} name **/const greet = (name) => `Hello ${name}!`;greet(10); // Warning in the editor
Multiple parameters can be listed with a comma, mirroring how they get passed to the function.
/** * @param {string} name * @param {number} size **/const greet = (name, size) => `Hello ${name}! Your chosen size is ${size}`;greet(`Yoko`, 51); // `Hello Yoko! Your chosen size is 51`
Parameters can also have default values. This makes it easier from the caller side, since it doesn’t have to provide values it doesn’t really ‘care’ about. Note the [ ]
around the parameter name in the type annotation to denote optionality.
/** * @param {string} name * @param {number} [size] **/const greet = (name, size = 12) => `Hello ${name}! Your chosen size is ${size}`;greet(`Yoko`); // `Hello Yoko! Your chosen size is 12`
A key rule about default parameters is they have to be on the far-right on the list of parameters. This won’t work as expected (and indeed VS Code will warn you about it):
const greet = (name=`Julian`, size) => `Hello ${name}! Your chosen size is ${size}`;greet(12); // `Hello 12! Your chosen size is undefined`
Returning objects
One ‘gotcha’ with the fat arrow function style is if you return an object, it has to be enclosed in ( )
. That’s because the the { }
we need to define an object is ambiguous with the start and end of a function block.
// This is fine, just returning a simple value like a stringconst generate = () => `John`;
// Won't work, because { } could also mean the body of a function in this contextconst generate = () => { name: `John`, size: 123 };// So we need to wrap it:const generate = () => ({ name: `John`, size: 123 });
This is not a problem with the old-school approach because the semantics are not ambigious. However, it remains more cluttered:
function generate() { return { name: `John`, size: 123 }}
Returning functions
In Javascript and ixfx, it’s common for functions to return functions. This can certainly be hard to wrap your head around at times.
// Return a function that returns an object { name, size }const generate = (name) => () => ({ name, size: Math.random() })...// 'g' is essentially () => ({ name: `John`, size: Math.random() })const g = generate(`John`);const value = g(); // { name: `John`, size: 0.2841 }
This technique is useful because we can ‘bake in’ parameters without having to pass them in each and every time.
A perfect example is interpolate. Let’s say we always want to interpolate values at the same rate. We’d have to repeat this parameter continually, or add it to something like settings:
// Interpolate between 10-20 by 20%// uses the signature (amount:number, a:number, b:number) => numberconst value = interpolate(0.2, 10, 20);
One of the ways of calling interpolate
lets us bake in the amount:
// Returns a function (a:number, b:number) => numberconst slow = interpolate(0.2);// Interpolate between 10-20 by 20%slow(10, 20);
This can be useful for DRY principles, but also the idea of encapsulation. Now we have a function that can interpolate with two inputs, which can be shared with other parts of your code without them needing to know how the interpolation happens. Only the code that creates the interpolation function needs to decide.
Maybe we set up our interpolate function once, as a setting, so it’s a reusable function later. In turn, this makes update()
simpler and more readable.
const settings = Object.freeze({ angleInterpolate: interpolate(0.2)})
const update = () => { const { angleInterpolate } = settings; let { angleCurrent, angleTarget } = state;
// Compute new current angle based on existing and target, // using the interpolator from `settings`. saveState({ angleCurrent: angleInterpolate(angleCurrent, angleTarget) // Instead of: // angleCurrent: interpolate(0.2, angleCurrent, angleTarget) })}
We could imagine a refactoring where angleInterpolate
instead computes a random value between the a
and b
values. The neat thing is we wouldn’t need to change update()
at all, since angleInterpolate
still has the same signature of (number, number) => number
.
const settings = Object.freeze({ angleInterpolate: (a, b) => Random.float({ min:a, max:b })})
If you find yourself calling a function repeatedly with the same parameters, you might want to write a little function that wraps it and passes in the standard parameter.
For example, you might have lots of:
someElement.addEventListener(`click`, event => { // do some stuff})
A little helper function could be:
const onClick = (el, func) => { el.addEventListener(`click`, func);}
Which you can then then use with:
onClick(someElement, event => { // do some stuff});
If it ends up saving you some typing and making your code more readable, it might be worth it, even for such trivial cases.
If you end up with a few of these, can stash them away in a separate file to be imported and shared:
export const onClick = (el, func) => { el.addEventListener(`click`, func);}
import { onClick } from './util.js'onClick(someElement, event => { // do some stuff})
On objects
Functions can ‘hang’ off objects. This is a great way of grouping functions together.
const util = { doX: () => { ... }, doY: () => { ... }}
util.doX();
This is essentially what happens when we import as a module:
export const doX = () => { ... }export const doY = () => { ... }
import * as util from "./util.js"util.doX();