01 - Create linear data flow with container style types (Box)¶

Here’s the video lesson.

Note

Assume this function is always defined:

const l = console.log.bind(console);

Intro¶

We want a function that given a numeric string like “64”, increments that number and then returns the character that the number represents. For example, if you man ascii you’ll see that the decimal number 65 represents the character ‘A’, 66 the character ‘B’ and so on and so forth. So, if we input the string “64”, it gets incremented to the number 65 and then converted to the character A.

As a side note, take a look at what String.fromCharCode() does:

String.fromCharCode(65);
// → 'A'

for (let i = 97; i <= 102; ++i)
l(String.fromCharCode(i));
// → a
// → b
// → c
// → d
// → e
// → f

Tip

UTF-8 is backwards-compatible with ASCII. The first 128 UTF-8 chars precisely match the first 128 ASCII chars. Read more.

The Test Suite¶

These cases seem to be enough to cover the domain of our function.

// import { nextCharFromNumStr } from './box-v1';
// import { nextCharFromNumStr } from './box-v2';
// import { nextCharFromNumStr } from './box-v3';
// import { nextCharFromNumStr } from './box-v4';
import { nextCharFromNumStr } from './box-v5';

describe('nextCharFromNumStr()', () => {
[
['64 ', 'A'],
[' 89 ', 'Z'],
['   96', 'a'],
['  121 ', 'z'],
].forEach(([input, expected]) => {
it(`should convert '\${input}' to '\${expected}'`, () => {
expect(nextCharFromNumStr(input)).toEqual(expected);
});
});
});

A Note on the type 1String¶

JavaScript doesn’t have a type for Char, therefore, we use the HtDP 1String type:

//
// A 1String is a String of length 1, including:
// • "a"
// • "Z"
// • "\\" (the backslash),
// • " " (the space bar),
// • "\t" (tab),
// • "\r" (return), and
// • "\b" (backspace).
//
// INTERP: Represents keys on the keyboard and other 1 character strings.
//

Temporary Variables¶

This first implementation uses temporary variables to store the result of each step in the process.

/**
* Produces the next char based on the numeric input string.
*
* ASSUME: The input is a valid numeric value.
*
* ASSUME: Client code is passing in a numeric value which
* produces the character they want.
*
* @param {string} value The numeric string.
* @return {string} The computed character.
*
* @sig 1String -> 1String
*
* @example
* nextCharFromNumStr('64');
* // → 'A'
*
* @example
* nextCharFromNumStr(' 121  ');
* // → 'z'
*/
function nextCharFromNumStr(value) {
const trimmed = value.trim();
const num = parseInt(trimmed, 10);
const nextNum = num + 1;
return String.fromCharCode(nextNum);
}

export { nextCharFromNumStr }

CONS:

• Too many temporary variables (variables to store intermediary results of each step);

• Beginner-like approach and coding style;

See this: this file

Nested Function Invocations¶

/**
* Produces the next char based on the numeric input string.
*
* ASSUME: The input is a valid numeric value.
*
* ASSUME: Client code is passing in a numeric value which
* produces the character they want.
*
* @param {string} value The numeric string.
* @return {string} The computed character.
*
* @sig 1String -> 1String
*
* @example
* nextCharFromNumStr('64');
* // → 'A'
*
* @example
* nextCharFromNumStr(' 121  ');
* // → 'z'
*/
function nextCharFromNumStr(value) {
return String.fromCharCode(parseInt(value.trim(), 10) + 1);
}

export { nextCharFromNumStr }

CONS:

• Nesting of invocation is harder to read, easy to get lost.

• Not scalable. Hard to add new stuff in anywhere in the chain.

Manual Container¶

This example use the concept of a “box” to map over values. We “manually” add the input value into a one-element array.

/**
* Produces the next char based on the numeric input string.
*
* ASSUME: The input is a valid numeric value.
*
* ASSUME: Client code is passing in a numeric value which
* produces the character they want.
*
* @sig 1String -> 1String
*
* @param {string} value The numeric string.
* @return {string} The computed character.
*
* @example
* nextCharFromNumStr('64');
* // → 'A'
*
* @example
* nextCharFromNumStr(' 121  ');
* // → 'z'
*/
function nextCharFromNumStr(value) {
return [value].map(s => s.trim())
.map(s => parseInt(s, 10))
.map(i => i + 1)
.map(i => String.fromCharCode(i)); /* (1) */
}

export { nextCharFromNumStr }

PROS:

• Easier to read the sequence of things that happen.

• Easy to add new operations any where in the chain.

(1): Note how we use index notation  at the very last thing in our processing. That is because we introduced the concept of containers but did not (yet) come up with a way to extract the value from the container without this manual, indexing approach (we’ll improve on this soon).

Manual Container With Helper Functions¶

All those arrow functions as callbacks to .map could be extracted, making the chaining look more neat and clean.

const trim = s => s.trim();
const toInt = s => parseInt(s, 10);
const add1 = n => n + 1;
const intToChar = i => String.fromCharCode(i);

/**
* Produces the next char based on the numeric input string.
*
* ASSUME: The input is a valid numeric value.
*
* ASSUME: Client code is passing in a numeric value which
* produces the character they want.
*
* @sig 1String -> 1String
*
* @param {string} value The numeric string.
* @return {string} The computed character.
*
* @example
* nextCharFromNumStr('64');
* // → 'A'
*
* @example
* nextCharFromNumStr(' 121  ');
* // → 'z'
*/
function nextCharFromNumStr(value) {
return [value].map(trim)
.map(toInt)
.map(intToChar); /* <1> */
};

export { nextCharFromNumStr }

Container Type¶

const trim = s => s.trim();
const toInt = s => parseInt(s, 10);
const add1 = n => n + 1;
const intToChar = i => String.fromCharCode(i);

/**
* A Value consumed and produced by the Container `map's function.
*
* @typedef {any} Value
*/

/**
* @typedef {Object} Container
* @property {function(function(Value): Value): Container} map Map a
*   function over the Value.
* @property {function(): string} toString Our custom stringification
*   of the object.
*/

/**
* Creates a chainable container.
*
* @param {Value} val
* @return {Container}
*/
const Box = val => {
return {
map: f => Box(f(val)),
fold: f => f(val),
toString: () => `Box(\${val})`,
};
};

/**
* Produces the next char based on the numeric input string.
*
* ASSUME: The input is a valid numeric value.
*
* ASSUME: Client code is passing in a numeric value which
* produces the character they want.
*
* @sig 1String -> 1String
*
* @param {string} value The numeric string.
* @return {string} The computed character.
*
* @example
* nextCharFromNumStr('64');
* // → 'A'
*
* @example
* nextCharFromNumStr(' 121  ');
* // → 'z'
*/
function nextCharFromNumStr(value) {
return Box(value)
.map(trim)
.map(toInt)
.fold(intToChar); /* (1) */
};

export { nextCharFromNumStr }

Note

The inspect thing used in the video does not work in recent versions of node (2021, v14 at least). Overriding toString should work. But then we must make sure we try to log the box as a string to trigger the toString mechanism.

This example use the concept of a “box” to map over values. We also add a fold function that can also “map” a function over a value, but instead of returning another Container, it returns the value itself. fold should generally be used as the last thing in the chain.

(1): We use fold here to finally return the value itself, unboxing it from the container.

map is not supposed to only loop over things. It has to do with composition within a context. Box is the context in this case. Box is a “container” type to capture different behaviours. Box is the identity functor.

PROS:

• Easier to read the sequence of things that happen.

• Easy to add new operations anywhere in the chain.

• We can unify method invocations s.trim(), function invocations parseInt(...), operators 1 + 1, and qualified invocations String.fromCharCode() (in our case, turned into cleaner helper functions).

nextChar example¶

TODO: Create an example with a function nextChar, which given a char like ‘a’ returns ‘b’. Keep it simple and don’t bother with the boundaries of the alphabet.