title: real world fp, 04 contracts theme: sudodoki/reveal-cleaver-theme style: https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.6.0/styles/zenburn.min.css controls: true
--
Monads, Comonad, Monoids, Setoids, Endofunctors.
Easy to understand, read, test and debug
by Vladimir Starkov
frontend engineer at Nordnet Bank AB
--
// double takes Number returns Number
const double = x => x * 2;
// split takes String returns Array of String's
const split = str => str.split(' ');
--
double(); // NaN
double('2q'); // NaN
split(); // TypeError: str is undefined
split(123); // TypeError: str.split is not a function
--
double()
and split()
will work properly only when input belongs to specified Type.
Let's establish this convention.
--
Brief function's documentation about arguments' types and returned value's type
// functionName :: argType1 -> argType2 -> argType3 -> resultType
--
// double takes Number returns Number
// double :: Number -> Number
const double = x => x * 2;
// split takes String returns Array of String's
// split :: String -> [String]
const split = str => str.split(' ');
--
Contract — as far as arguments satisfy signature, function will work as expected.
Every mususage leads to broken function
--
- empty input:
undefined
orfn()
- invalid input:
wrong type
orfn(invalidType)
--
- Function can be used only in specific condition
- Function will be broken otherwise
- Function consumer will get unhelpful messages
Function consumer — your co-worker in the office or another developer in the open-source.
--
- Function can be used only in specific condition
Thats fine, as far as it really works!
--
- Function will be broken otherwise
- Function consumer will get unhelpful messages
- force desired types
- if types are invalid, print helpful and careful messages for consumer
--
--
- TypeScript
- Flow
--
- Both are great
- Its vendor lock, though
- Introduces compilation build-step
- TypeScript doesn't expose run-time checks and will not do it It means no help for your code consumers =(
Why do we need to introduce new language superset
if we can do it with one function?
--
Function — which type-check arguments.
- If type is correct just returns value
- Otherwise
throw new TypeError(helpfulMessage)
- Where helpful message, should contain
- argument name
- desired type
- actual type
- actual argument value
--
/**
* typecheck is value belongs to Constructor
* @example:
* is(Number, 2); // true
* is(Number, 'qwe'); // false
* @signature:
* is :: Constructor -> value -> Boolean
*/
const is = (Ctor, val) => val != null && val.constructor === Ctor || val instanceof Ctor;
--
/**
* returns value's type
* @example:
* type(2); // 'Number'
* type('qwe'); // 'String'
* @signature:
* type :: value -> String
*/
const type = val => (val !== null && val !== undefined)
? Object.prototype.toString.call(val).slice(8, -1)
: (val === null) ? 'Null': 'Undefined';
--
/**
* returns Constructor's type
* @example:
* type(Number); // 'Number'
* type(String); // 'String'
* @signature:
* ctorType :: Constructor -> String
*/
const ctorType = Ctor => (Ctor !== null && Ctor !== undefined)
? type(Ctor())
: (val === null) ? 'Null': 'Undefined';
--
const contract = (argName, Ctor, actualArg) => {
if (is(Ctor, actualArg)) {
return actualArg;
} else {
throw new TypeError(
`${argName} should be an ${ctorType(Ctor)}, but got ${type(actualArg)}: ${actualArg}`
);
}
}
contract('x', Number, 2); // 2
contract('x', Number, 'nope');
//> TypeError: x should be an Number, but got String: nope
--
So we need to contract function and then invoke actual implementation, so its two steps — ideal case for pipe
.
--
// double :: Number -> Number
const double = pipe(
x => contract('x', Number, x),
x => x * 2
)
// split :: String -> [String]
const split = pipe(
str => contract('str', String , str),
str => inputStr.split(' ')
)
--
const contract = curry( (argName, Ctor, actualArg) => {
if (is(Ctor, actualArg)) {
return actualArg;
} else {
throw new TypeError(
`${argName} should be an ${ctorType(Ctor)}, but got ${type(actualArg)}: ${actualArg}`
);
}
} );
--
// double :: Number -> Number
const double = pipe(
// x => contract('x', Number, x),
// x => contract('x', Number)(x),
contract('x', Number),
x => x * 2
);
// split :: String -> [String]
const split = pipe(
// str => contract('str', String, str),
// str => contract('str', String)(str),
contract('str', String),
str => inputStr.split(' ')
);
--
double(2); // 4
double('nope'); //> TypeError: x should be an Number, but got String: nope
split('sup js'); // ['sup', 'js'];
split(2); //> TypeError: str should be an String, but got Number: 2
--
- Function is happy and will work if contract is satisfied
- You co-workers are happy to use your code
- If your code is not working for them they can fix it easily
--
- The Error Model
- Is your JavaScript function actually pure?
- neat-contract lib, a bit extended function from this talk
--
"real world fp" workshop repo
"#4 contracts" slides
To be continued with real world implementation with you.
Stay tuned
--
*In functions we trust*
Sincerely yours Vladimir Starkov
@iamstarkov on github and twitter