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))[0]; /* (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 [0]
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(add1)
.map(intToChar)[0]; /* <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)
.map(add1)
.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 invocationsparseInt(...)
, operators1 + 1
, and qualified invocationsString.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.