Skip to content

Latest commit

 

History

History
346 lines (242 loc) · 6.88 KB

04-contracts.md

File metadata and controls

346 lines (242 loc) · 6.88 KB

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

--

real world fp

#4 contracts

Monads, Comonad, Monoids, Setoids, Endofunctors.
Easy to understand, read, test and debug




by Vladimir Starkov
frontend engineer at Nordnet Bank AB

--

what

// double takes Number returns Number
const double = x => x * 2;

// split takes String returns Array of String's
const split = str => str.split(' ');

--

Problem

double();     // NaN
double('2q'); // NaN

split();      // TypeError: str is undefined
split(123);   // TypeError: str.split is not a function

--

Problem, exactly

double() and split() will work properly only when input belongs to specified Type.

Let's establish this convention.

--

Hindley-Milner signature

Brief function's documentation about arguments' types and returned value's type

// functionName :: argType1 -> argType2 -> argType3 -> resultType

--

Hindley-Milner signature

// 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(' ');

--

Signature as Contract

Contract — as far as arguments satisfy signature, function will work as expected.

Every mususage leads to broken function

--

Misusages

  • empty input: undefined or fn()
  • invalid input: wrong type or fn(invalidType)

--

Problem definition, revisited

  1. Function can be used only in specific condition
  2. Function will be broken otherwise
  3. Function consumer will get unhelpful messages

Function consumer — your co-worker in the office or another developer in the open-source.

--

Problem definition, solution

  1. Function can be used only in specific condition

Thats fine, as far as it really works!

--

Problem definition, solution

  1. Function will be broken otherwise
  2. Function consumer will get unhelpful messages
  • force desired types
  • if types are invalid, print helpful and careful messages for consumer

--

JavaScript Type System™

--

JavaScript Type System™

Static typing JS dialects

  • TypeScript
  • Flow

--

TypeScript and Flow


Why do we need to introduce new language superset
if we can do it with one function?

--

Solution

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

--

contract function, preparation

is helper

/**
 * 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;

--

contract function, preparation

type helper

/**
 * 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';

--

contract function, preparation

ctorType helper

/**
 * 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';

--

contract function, implementation

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

--

Contract function, usage

So we need to contract function and then invoke actual implementation, so its two steps — ideal case for pipe.

--

Contract function, usage

// 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(' ')
)

--

Contract function, improvement

curry it!

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}`
    );
  }
} );

--

Contract function, improvement

// 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(' ')
);

--

Result

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

--

Summary

  • 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

--

Further reading

--

Functional Programming, (recursion)

"real world fp" workshop repo
"#4 contracts" slides

To be continued with real world implementation with you.

Stay tuned

--

real world fp

#4 contracts


*In functions we trust*

Sincerely yours Vladimir Starkov
@iamstarkov on github and twitter