Revealing Intent

or

Typed Programming in Standard Javascript

What We'll Cover

  • Types and Type Definitions
  • Predicates as Types
  • Subsets and Subtypes
  • Higher-Kinded Types
  • Building a Domain Model

What I Used

These Slides are Available Online

Chris Stead

@cm_stead

chrisstead.net

A Little About Me

  • Javascript Programmer
  • Math Geek
  • Tool Builder

Let's Look at the Gorilla in the Room

This is Koko

Koko and her cat Smoky

Fact:

Writing and maintaining
large Javascript applications is hard!

Reasons Javascript is Hard

  • No required build step, so it's often tested at runtime
  • Remembering where things are is tough
  • Remembering an old function contract is impossible

Typed Vs. Untyped

Javascript isn't untyped, it's, at worst, singly-typed
At its core, the Javascript base type is Object, but we could actually say the base type is "Dynamic"

A Simple Function

						
		function vectorAdd(v1, v2) {
			return [v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]];
		}

		vectorAdd([1, 2, 3], [4, 5, 6]);
		vectorAdd([1, 2, 3, 4], [5, 6, 7]);
		vectorAdd('foo', 'bar');
						
					

Naïve Type Checking

						
    function vectorAdd(v1, v2) {
        throwOnArrayMismatch(v1);
        throwOnArrayMismatch(v2);
        
        return [v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]];
    }

	vectorAdd([1, 2, 3], [4, 5, 6]);
	vectorAdd([1, 2, 3, 4], [5, 6, 7]);
	vectorAdd('foo', 'bar');
						
					

Functions are Data Too

Function Types are Defined by Mappings

  • f: (a => b)
  • x: a
  • (f x): b

Function Properties as Metadata

Object is the base type for all other types, including Function
We can attach function type information as a metadata string

Attaching a Signature as Metadata

						
		vectorAdd.signature = 'array, array => array';
		
		function vectorAdd(v1, v2) {
			throwOnArrayMismatch(v1);
			throwOnArrayMismatch(v2);
			
			return [v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]];
		}

        (function (fn) { return fn.signature})(vectorAdd);
		
        vectorAdd([1, 2, 3], [4, 5, 6]);
        vectorAdd([1, 2, 3, 4], [5, 6, 7]);
        vectorAdd('foo', 'bar');
						
					

We can do better!

Enforcing Behavior with Signet

						
    var vectorAdd = signet.enforce(
        'array, array => array',
        function vectorAdd(v1, v2) {
            return [v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]];
        });

	(function (fn) { return fn.signature })(vectorAdd);
	
	vectorAdd([1, 2, 3], [4, 5, 6]);
	vectorAdd([1, 2, 3, 4], [5, 6, 7]);
	vectorAdd('foo', 'bar');
						
					

Defining Types with Predicates

Functions as a Mapping Mechanism

Functions provide a way to map values from one set to another.

φ is defined as follows:

  • φ: A → B when p(x) = true
  • φ: A → B-1 when p(x) = false

Types as Sets

A type can be considered a set to which values belong

A type set can be defined with a predicate function

This rises from the Curry-Howard Isomorphism

Types, Sets and Predicates

  • Javascript native types are named sets like these
    • String
    • Number
    • ...
  • Predicates can extend these sets and define new types
    • int
    • tuple
    • ...

A Utility Function

						
	function checkType(type, value) {
		if (!signet.isTypeOf(type)(value)) {
			var errorMessage = 'Expected value of type ' + 
								type + ' but got ' + 
								typeof value + ' (' + 
								value.toString() + ')';
				
			throw new Error(errorMessage);
		}

		return value.toString() + ' is of type ' + type;
	}						
					

Building A Type With Predicates

						
    function lowerCaseType (value){
        return typeof value === 'string' && value.toLowerCase() === value;
    }
    
    signet.extend('lowerCase', lowerCaseType);
    
	checkType('lowerCase', 'foo'),
	checkType('lowerCase', 'bar'),
	checkType('lowerCase', 'BaZ'),
						
					

Type Polymorphism

The is-a relationship

Building A Type with Polymorphism

A subtype can be defined as a subset of another type

number -> int -> natural -> age

Constructing The Age Type

						
	signet.subtype('number')('int', value => value === Math.floor(value));
	
    signet.subtype('int')('natural', value => value >= 0;);
	
    signet.subtype('natural')('age', value => value <= 150;);
						
					

Testing Our New Types

						
	checkType('int', 3.2);
	checkType('int', -3);
	checkType('natural', -3);
	checkType('natural', 200);
	checkType('age', 200);
	checkType('age', 42);
						
					

Higher-Kinded Types

Defining A Higher-Kinded Type

  • Data type
  • Related to currying and partial application
  • Takes a type and returns a type (* -> *) -> *

Tuple: A Higher-Kinded Type

Tuple<number;number> -> point

Constructing The Tuple Type

						
    function checkType (tuple, type, index){
        return signet.isTypeOf(type)(tuple[index]);
    }
    
    function verifyTuple (tuple, typeObj) {
        return typeObj.valueType
                    .map(checkType.bind(null, tuple))
                    .reduce(function (a, b) { return a && b; }, true);
    }
    
    signet.subtype('array')('tuple', tupleType);

    function tupleType(tuple, typeObj) {
        var correctLength = tuple.length === typeObj.valueType.length;
        var correctTypes = verifyTuple(tuple, typeObj);
        
        return correctLength && correctTypes;
    }
						
					

Testing our Tuple

						
    signet.subtype('string')('name', function (value) {
        return value.length < 20;
    });
    
	checkType('tuple<number;number>', [3, 4]),
	checkType('tuple<age;name>', [21, 'Bobby']),
	checkType('tuple<age;name>', [5, 99])
						
					

Constructing a Domain Model Hierarchy

string -> formattedString<^[0-9]{3}\\-[0-9]{2}\\-[0-9]{4}$> -> SSN

Building A Formatted String Type

						
    signet.subtype('string')('formattedString', function (value, typeObj) {
        var pattern = new RegExp(typeObj.valueType[0]);
        return value.match(pattern) !== null;
    });
						
					

Creating Our Domain Type

						
    var ssnPattern = '^[0-9]{3}\\-[0-9]{2}\\-[0-9]{4}$';
    var ssnDefinition = 'formattedString<' + ssnPattern + '>';

    signet.alias('ssn', ssnDefinition);
    
	checkType('ssn', '123-45-6789');
	checkType('ssn', '123456789');
	checkType('ssn', 'FOO');
						
					

Everything Old is New Again

Vector Addition Revisited

						
    signet.alias('3dVector', 'tuple<number;number;number>');
    
    var vectorAdd = signet.enforce(
		'3dVector, 3dVector => 3dVector',
		function vectorAdd(v1, v2) {
			return [v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]];
		});

	vectorAdd([1, 2, 3], [4, 5, 6]);
	vectorAdd([1, 2, 3, 4], [5, 6, 7]);
	vectorAdd('foo', 'bar');
						
					

What does it all mean?!?

Types Add Guarantees and API Reference Information

  • Guarantee of correctness
  • Easy inline reference for function signature
  • Lasting notes for other programmers

Inline Types Fix Transpiler Type Erasure

When Typescript, Purescript and others are transpiled all type information is stripped

Types Reveal Intent

Type checking and annotations provide insight into how a function behaves and ensures function use matches original intent

When the going gets tough, the tough use types!

Chris Stead

@cm_stead

chrisstead.net