Toward modular, reusable code
4.2 Requirements for compatible functions
Object-oriented programs use pipelines sporadically, in specific scenarios (authentica-tion/authorization is usually one of them); on the other hand, functional program-ming relies on pipelines as the sole method of building programs. Depending on the task at hand, there’s usually quite a gap between a problem definition and a proposed solution; therefore, computations must be carried out in well-defined stages. These stages are represented by functions that execute with the condition that their inputs and outputs be compatible in two ways:
■ Type—The type returned by one function must match the argument type of a receiving function.
■ Arity—A receiving function must declare at least one parameter in order to handle the value returned from a preceding function call.
4.2.1 Type-compatible functions
When designing function pipelines, it’s important that there exists a level of compati-bility between what functions return and what they accept. In terms of type, this isn’t as big a concern in JavaScript as it is with statically typed languages, because JavaScript is loosely typed. Hence, if an object behaves like a certain type in practice, it’s that type. This is also known as duck typing: “If it walks like a duck and talks like a duck, it’s a duck.”
NOTE Statically typed languages have the advantage of using type systems to alert you about potential problems without having to run your code. Type sys-tems are an important topic in functional programming but aren’t covered in this book.
JavaScript’s dynamic dispatch mechanism attempts to find properties and methods in your objects regardless of type information. Although this is extremely flexible, you often need to know what types of values a function is expecting; having this clearly defined (perhaps documented in code using the Haskell notation) makes your pro-grams easier to understand.
Formally speaking, two functions f and g are type-compatible if the output of f has a type equivalent to the set of inputs of g. For example, here’s a simple program to process a student’s Social Security number:
trim :: String -> String normalize :: String -> String
At this point, you should be able to follow the correspondence between the input of normalize and the output of trim so that you can invoke them in a simple, manual, pipeline sequence, as shown in the following listing.
// trim :: String -> String
const trim = (str) => str.replace(/^\s*|\s*$/g, '');
// normalize :: String -> String
const normalize = (str) => str.replace(/\-/g, '');
normalize(trim(' 444-44-4444 ')); //-> '444444444'
Types are certainly important but, in JavaScript, not as critical as being compatible with the number of arguments a function accepts.
4.2.2 Functions and arity: the case for tuples
Arity can be defined as the number of arguments a function accepts; it’s also referred to as the function’s length. We usually take arity for granted in other programming par-adigms, but in functional programming, as a corollary to referential transparency, the number of arguments a function declares is often directly proportional to its complex-ity. For instance, a function that works on a single string is likely much simpler than one taking three or four arguments:
// isValid :: String -> Boolean function isValid(str) {
...
}
// makeAsyncHttp:: String, String, Array -> Boolean function makeAsyncHttp (method, url, data) {
...
}
Pure functions that expect a single argument are the simplest to use because the implication is that they serve a single purpose—a singular responsibility. Our goal is to work with functions with as few arguments as possible, because they’re more flexible
Listing 4.1 Building a manual function pipeline with trim and normalize Trims leading and
trailing whitespace Removes any dashes from the input string
Manually calls both functions in a simple sequential pipeline (you’ll see how to automate this technique later). Calls the function purposely with leading and trailing whitespace.
Easy to use
Harder to use, because all arguments must be computed first
and versatile than those that depend on multiple arguments. Unfortunately, unary functions aren’t easy to come by. In real life, isValid can be embellished with an error message that clearly describes what happened:
isValid :: String -> (Boolean, String)
isValid(' 444-444-44444'); //-> (false, 'Input is too long!')
But how can you return two different values? Functional languages have support for a structure called a tuple. It’s a finite, ordered list of elements, usually grouping two or three values at a time, and written (a, b, c). Based on this concept, you can use a tuple as a return value from isValid that groups a status with a possible error mes-sage, to be returned as a single entity and subsequently passed to another function if need be. Let’s explore tuples in more detail.
Tuples are immutable structures that pack together items of different types so that they can be passed into other functions. There are other ways of returning ad hoc data, such as object literals or arrays:
return {
status : false, or return [false, 'Input is too long!'];
message: 'Input is too long!' };
But when it comes to transferring data between functions, tuples offer more advantages:
■ Immutable—Once created, you can’t change a tuple’s internal contents.
■ Avoid creating ad hoc types—Tuples can relate values that may have no relation-ship at all to each other. So defining and instantiating new types solely for grouping data together makes your model unnecessarily convoluted.
■ Avoid creating heterogeneous arrays—Working with arrays containing different types of elements is hard because it leads to writing code filled with lots of defensive type checks. Traditionally, arrays are meant to store objects of the same type.
Moreover, tuples behave much like the value objects shown in chapter 2. One con-crete use case is in the concept of a Status, a simple data type containing a status flag and a message: (false, 'Some error occurred!'). Unlike other functional languages, such as Scala, JavaScript has no native support for a Tuple data type. For instance, given the following Scala tuple definition
var t = (30, 60, 90)
you can access each individual part like this:
var sumAnglesTriangle = t._1 + t._2 + t._3 = 180
Returns a structure that holds the status of the validation and possibly an error message
But JavaScript provides all the tools out of the box required for you to implement your own version of Tuple, as shown next.
const Tuple = function( /* types */ ) {
const typeInfo = Array.prototype.slice.call(arguments, 0);
const _T = function( /* values */ ) {
const values = Array.prototype.slice.call(arguments, 0);
if(values.some((val) =>
val === null || val === undefined)) {
throw new ReferenceError('Tuples may not have any null values');
}
if(values.length !== typeInfo.length) { throw new TypeError('Tuple arity does not
match its prototype');
}
values.map(function(val, index) {
this['_' + (index + 1)] = checkType(typeInfo[index])(val);
}, this);
Object.freeze(this);
};
_T.prototype.values = function() {
return Object.keys(this).map(function(k) {
The Tuple object in listing 4.2 is an immutable, fixed-length structure used to hold a heterogeneous set of n typed values that can be used for inter-function communica-tion. For instance, you can use it to build quick value objects, such as Status:
const Status = Tuple(Boolean, String);
Let’s finish the student SSN validation example to take advantage of tuples.
Listing 4.2 Typed Tuple data type Reads the of making sure the types match the
Checks for non-null values.
Functional data types shouldn’t permit null values to permeate.
Checks that the tuple has the correct arity with respect to the number of types defined
Checks that each value passed in matches the correct type in the tuple definition using the checkType function (shown later). Every tuple element will translate to a property of the tuple referred to by ._n, where n is the index of the element (starting at 1).
Makes the tuple instance immutable
Extracts all values from the tuple as an array. You can use this with ES6 assignment destructuring to map tuple values into variables.
// trim :: String -> String
const trim = (str) => str.replace(/^\s*|\s*$/g, '');
// normalize :: String -> String
const normalize = (str) => str.replace(/\-/g, '');
// isValid :: String -> Status const isValid = function (str) { if(str.length === 0){
return new Status(false,
'Invald input. Expected non-empty value!');
} else {
return new Status(true, 'Success!');
} }
isValid(normalize(strim('444-44-4444'))); //-> (true, 'Success!')
The occurrence of 2-tuples is so frequent in software that it’s worth making them first-class objects. When combined with JavaScript ES6 support for destructured assignment, you can map tuple values to variables in a clean manner. Using tuples, the following code creates an object called StringPair.
const StringPair = Tuple(String, String);
const name = new StringPair('Barkley', 'Rosser');
[first, last] = name.values();
first; //-> 'Barkley' last; //-> 'Rosser'
const fullname = new StringPair('J', 'Barkley', 'Rosser');
Tuples are one way to reduce a function’s arity, but there’s a better alternative for cases in which tuples aren’t sufficient. Let’s spice things up a bit by introducing function curry-ing, which not only abstracts arity but also encourages modularity and reusability.