Skip to content

TypeScript

Simplifying Console Logic with the Model-View-Update

When I first started dabbling in Functional Programming, a new front-end language called Elm had been released and it was generating a lot of buzz about how it simplified web development by introducing four parts (i.e., The Elm Architecture" (TEA)) that provided a mental model when creating web pages. This way of thinking was so powerful that it inspired popular libraries like Redux and ngrx which took this architecture mainstream.

Spilling the TEA

At a high level, the architecture has four parts:

  1. Model -> What are we rendering?
  2. View -> How do we want to render it?
  3. Update -> Given the current model and a Command, what's the new model?
  4. Command -> What did the user do?

To help make this a bit more clear, let's define some types for these parts and see how they would work together

type Model = any;
type View = (m:Model)=>Promise<Command>;
type Update = (m:Model, c:Command)=>Model;
type Command = "firstOption" | "secondOption" ... | 'quit';

async function main(model:Model, view:View, update:Update): Promise<void>{
  const command = await view(model);
  if (command === 'quit'){
    return;
  }
  const newModel = update(model, command);
  return main(newModel);
}

With some types in play, let's go ahead and build out a small application, a counter (the "Hello World" for Elm).

Building a Counter

First, we need to figure out what the model will be. Since we're only keeping tracking of a number, we can define our model as a number.

type Model = number;

Next, we need to define what the user can do. In this case, they can either increment, decrement, or quit so let's set the command up.

type Command = 'increment' | 'decrement' | 'quit';

Now that we have Command, we can work on the update function. Given the type signature from before, we know its going to look like this:

1
2
3
function update(model:Model, command:Command): Model {
  // logic
}

We can leverage a switch and put in our business rules

1
2
3
4
5
6
7
function update(model:Model, command:Command): Model {
  switch(command){
    case 'increment': return model+1;
    case 'decrement': return model-1;
    case 'quit': return model;
  }
}

Finally, we need to define our view function. Like before, we can get the skeleton for the function based on the types from earlier.

1
2
3
async function view(model:Model): Promise<Command>{

}

Let's update the function with our rendering logic

1
2
3
4
async function view(model:Model): Promise<Command>{
  console.log("Counter:", model);
  console.log("Choose to (i)ncrement, (d)ecrement, or (q)uit");
}

We've got our render up and running, however, we need to get input from the user. Since we're working within Node, we could use readline, however, I've recently been using @inquirer/prompts and find it to be a nice abstraction to use. So let's use that package.

import {input} from "@inquirer/prompts";

async function getChoice(): Promise<Command>{
  console.log("Choose to (i)ncrement, (d)ecrement, or (q)uit");
  const validChoices = ["i", "d", "q"];
  const validator = (s:string) => validChoices.include(s?.trim().toLowerCase());
  const selection = await input({message:message, validate:validator});
  if (selection === "i") {
    return "increment";
  } else if (selection === "d"){
    return "decrement";
  } else {
    return "terminate"
  }
}
// Let's change the view function to use getChoice

async function view(model:Model): Promise<Command>{
  console.log("Counter:", model);
  return getChoice();
}

With these pieces defined, we can use the main function from before.

async function main(model:Model, view:View, update:Update): Promise<void>{
  const command = await view(model);
  if (command === 'quit'){
    return;
  }
  const newModel = update(model, command);
  return main(newModel);
}

// Invoking Main
main(10, view, update);

Starting Back at Zero

Now that we have increment and decrement working, it would be nice to be able to reset the counter without having to restart the application, so let's see how bad that would be.

First, we need to add a new choice to Command (called reset). This will force us to update the rest of the code that's working with Command.

type Command = "increment" | "decrement" | "reset" | "quit";

Next, we need to update the update function so it knows how to handle a reset command. In our case, we need to set the model back to zero.

1
2
3
4
5
6
7
8
function update(model:Model, command:Command): Model {
  switch(command){
    case 'increment': return model+1;
    case 'decrement': return model-1;
    case 'reset': return 0;
    case 'quit': return model;
  }
}

At this point, the application knows how to handle the new Command, however, we need to update our view function to allow the user to select reset.

async function view(model:Model): Promise<Command>{
  console.log("Counter:", model);
  return getChoice();
}

async function getChoice(): Promise<Command>{
  // updating the console.log
  console.log("Choose to (i)ncrement, (d)ecrement, (r)eset, or (q)uit"); 
  const validChoices = ["i", "d", "r", "q"];
  const validator = (s:string) => validChoices.include(s?.trim().toLowerCase());
  const selection = await input({message:message, validate:validator});
  if (selection === "i") {
    return "increment";
  } else if (selection === "d"){
    return "decrement";
  } else if (selection === "r"){
    return "reset";
  } else {
    return "terminate"
  }
}

What's Next?

Now that we have have a working version, you could start implementing some fun functionality. For example, how would you allow someone to set how much to increment or decrement by? What if you needed to keep track of previous values (i.e., maintaining history)? I highly encourage you trying this out with a simple kata (for example, how about giving Mars Rover a try?)

Full Working Solution

import {input} from "@inquirer/prompts";

type Model = number;
type Command = "increment" | "decrement" | "reset" | "quit";
type View = (model:Model) => Promise<Command>;
type Update = (model:Model, command:Command) => Model;

function update(model:Model, command:Command): Model {
  switch(command){
    case "increment": return model+1;
    case "decrement": return model-1;
    case "reset": return 0;
    case "quit": return model;
  }
}

function view(model:Model): Promise<Command>{
  console.log(`Counter:${model}`);
  return getChoice();
}

async function getChoice(): Promise<Command>{
  console.log("Choose to (i)ncrement, (d)ecrement, (r)eset, or (q)uit"); 
  const validChoices = ["i", "d", "r", "q"];
  const validator = (s:string) => validChoices.include(s?.trim().toLowerCase());
  const selection = await input({message:message, validate:validator});
  if (selection === "i") {
    return "increment";
  } else if (selection === "d"){
    return "decrement";
  } else if (selection === "r"){
    return "reset";
  } else {
    return "terminate"
  }
}

async function main(model:Model, view:View, update:Update): Promise<void>{
  const command = await view(model);
  if (command === 'quit'){
    return;
  }
  const newModel = update(model, command);
  return main(newModel, view, update);
}

main(10, view, update);

Leveraging Tuples in TypeScript

In preparation for StirTrek, I'm revisiting my approach for how to implement the game of Blackjack. I find card games to be a great introduction to functional concepts as you hit the major concepts quickly and the use cases are intuitive.

Let's take a look at one of the concepts in the game, Points.

Blackjack is played with a standard deck of cards (13 Ranks and 4 Suits) where the goal is to get the closest to 21 points without going over. A card is worth Points based on its Rank. So let's go ahead and model what we know so far.

1
2
3
type Rank = "Ace" | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | "Jack" | "Queen" | "King"
type Suit = "Hearts" | "Clubs" | "Spades" | "Diamonds"
type Card = {readonly rank:Rank, readonly suit:Suit}

We know that a Card is worth points based on its rank, the rule are:

  • Cards with a Rank of 2 through 10 are worth that many points (i.e., 2's are worth 2 points, 3's are worth 3 points, ..., 10's are worth 10 points)
  • Cards with a Rank of Jack, Queen, or King are worth 10 points
  • Cards with a Rank of Ace can be worth either 1 or 11 points (depending on which one is the most advantageous)

Let's explore the Ace in more detail.

For example, if we had a hand consisting of an Ace and a King, then it could be worth either 11 (treating the Ace as a 1) or as 21 (treating the Ace as an 11). In this case, we'd want to treat the Ace as an 11 as that gives us 21 exactly (specifically, a Blackjack).

In another example, if we had a hand consisting of an Ace, 6, and Jack, then it could either be worth 17 (treating the Ace as a 1) or 27 (treating the Ace as an 11). Since 27 is greater than 21 (which would cause us to bust), we wouldn't want the Ace to be worth 11.

Creating cardToPoints

Now that we have this detail, let's take a look at trying to write the cardToPoints function.

function cardToPoints(c:Card): Points { // Note we don't know what the type of this is yet
  switch(c.rank) {
    case 'Ace': return ???
    case 'King': return 10;
    case 'Queen': return 10;
    case 'Jack': return 10;
    default:
      return c.rank; // we can do this because TypeScript knows all the remaining options for Rank are numbers
  }
}

At this point, we don't know how to score Ace because we would need to know the other cards to get points for. Since we don't have that context here, why not capture both values?

function cardToPoints(c:Card): Points { // Note we don't know what the type of this is yet
  switch(c.rank) {
    case 'Ace': return [1,11];
    case 'King': return 10;
    case 'Queen': return 10;
    case 'Jack': return 10;
    default:
      return c.rank; // we can do this because TypeScript knows all the remaining options for Rank are numbers
  }
}

In TypeScript, we can denote a tuple by using []. Going forward, TypeScript knows that it's a two element array and guarantees that we can index using 0 or 1.

This works, however, anything using cardToPoints has to deal with that it could either be a number or a tuple.

When I come across cases like this, I reach for setting up a sum type to model each case.

1
2
3
type Hard = {tag:'hard', value:number};
type Soft = {tag:'soft', value:[number,number]}; // note that value here is a tuple of number*number
type Points = Hard | Soft

Now, when I call cardToPoints, I can use the tag field to know whether I'm working with a number or a tuple.

Adding Points Together

A common workflow in Blackjack is to figure out how many points someone has. At a high level, we'd want to do the following

  • Convert each Card to Points
  • Add all the Points together

Summing things together is a common enough pattern, so we know our code is going to look something like this:

1
2
3
function handToPoints(cards:Card[]): Points {
  return cards.map((c)=>cardToPoints(c)).reduce(SOME_FUNCTION_HERE, SOME_INITIAL_VALUE_HERE);
}

We don't have the reducer function defined yet, but we do know that it's a function that'll take two Points and return a Points. So let's stub that out.

1
2
3
function addPoints(a:Points, b:Points): Points {
  // implementation
}

Since we modeled Points as a sum type, we can use the tag field to go over the possible cases

function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard') {
    // logic
  }
  if (a.tag === 'hard' && b.tag === 'soft'){
    // logic
  }
  if (a.tag === 'soft' && b.tag === 'hard'){
    // logic
  } 
  // last case is both of them are soft
}

With this skeleton in place, let's start implementing each of the branches

Adding Two Hard Values

The first case is the easiest, if we have two hard values, then we add their values together. So a King and 7 us a 17 for example.

1
2
3
4
function addHardAndHard(a:Hard, b:Hard): Points { // note that I'm defining a and b as Hard and not just Points
  const value = a.value + b.value;
  return {tag:'hard', value};
}

With this function defined, we can update addPoints like so

1
2
3
4
5
6
function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard'){
    return addHardAndHard(a,b);
  }
  // other branches
}

Adding Hard and Soft

The next two cases are the same, where we're adding a Hard value to a Soft value. For example, we're adding a 6 to an Ace. We can't assume that the answer is 7 since that might not be what the player wants. We also can't assume that the value is 17 because that might not be to the players advantage, which means that we need to keep track of both options, which implies that the result would be a Soft value. Let's go ahead and write that logic out

1
2
3
4
function addHardAndSoft(a:Hard, b:Soft): Points { // note that a is typed to be Hard and b is typed as Soft
  const [bLow, bHigh] = b.value; // destructuring the tuple into specific pieces
  return {tag:'soft', value:[a.value+bLow, a.value+bHigh]};
}

With this function in place, we can write out the next two branches

function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard'){
    return addHardAndHard(a, b);
  }
  if (a.tag === 'hard' && b.tag === 'soft'){
    return addHardAndSoft(a, b);
  }
  if (a.tag === 'soft' && b.tag === 'hard'){
    return addHardAndSoft(b, a); 
  }
  // remaining logic
}

Adding Soft and Soft

The last case we need to handle is when both Points are Soft. If we were to break this down, we have four values (aLow, aHIgh for a, and bLow,bHigh for b) we need to keep track of:

  1. aLow + bLow
  2. aHigh + bLow
  3. aLow + bHigh
  4. aHigh + bHigh

However, let's play around with this by assuming that Points in question are both Ace. We would get the following:

  1. aLow + bLow = 1 + 1 = 2
  2. aHigh + bLow = 11 + 1 = 12
  3. aLow + bHigh = 1 + 11 = 12
  4. aHigh + bHigh = 11 + 11 = 22

Right off the bat, we can discard the case 4, (aHigh + bHigh), because there is no situation where the player would want that score as they would bust.

For cases 2 and 3, they yield the same value, so they're essentially the same case.

Which means, that our real cases are

  1. aLow + bLow
  2. aHigh + bLow (which is the same as aLow + bHigh)

So let's go ahead and write that function

1
2
3
4
5
function addSoftAndSoft(a:Soft, b:Soft): Points {
  const [aLow, aHigh] = a.value;
  const [bLow] = b.value; // note that we're only grabbing the first element of the tuple here
  return {tag:'soft', value:[aLow+bLow, aHigh+bLow]};
}

Which gives us the following for addPoints

function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard'){
    return addHardAndHard(a, b);
  }
  if (a.tag === 'hard' && b.tag === 'soft'){
    return addHardAndSoft(a, b);
  }
  if (a.tag === 'soft' && b.tag === 'hard'){
    return addHardAndSoft(b, a);
  }
  return addSoftAndSoft(a as Soft, b as Soft);
}

Now that we have addPoints, let's revisit handToPoints

1
2
3
4
5
6
7
8
// Original Implementation
// function handToPoints(cards:Card[]): Points {
//   return cards.map((c)=>cardToPoints(c)).reduce(SOME_FUNCTION_HERE, SOME_INITIAL_VALUE_HERE;
// }

function handToPoints(cards:Card[]): Points {
  return cards.map((c)=>cardToPoints(c)).reduce(addPoints, SOME_INITIAL_VALUE_HERE);
}

Now we need to figure out what SOME_INITIAL_VALUE_HERE would be. When working with reduce, a good initial value would be what would we return if we had no cards in the hand? Well, they would have 0 points, right? We can use 0, but we can't just return 0 since our function returns Points, so we need to go from 0 to Points. Easy enough, we can use Hard to accomplish this.

1
2
3
4
5
6
7
function handToPoints(cards:Card[]): Points {
  const initialValue:Points = {tag:'hard', value:0};
  return cards.map((c)=>cardToPoints(c)).reduce(addPoints, initialValue);
}

const hand = [{rank:'Ace', suit:'Hearts'}, {rank:7, suit:'Clubs'}]
console.log(handToPoints(hand)); // {tag:'soft', value:[8, 18]};

For those who know a bit of category theory, you might notice that addPoints is the operation and Hard 0 is the identity for a monoid over Points.

One Last Improvement

So this code works and everything is fine, however, we can make one more improvement to addPoints. Let's take a look at what happens when we try to get the Points for the following:

1
2
3
4
5
6
7
8
const hand: Card[] = [
  {rank:'Ace', suit:'Diamonds'},
  {rank:8, suit:'Hearts'},
  {rank:4, suit:'Clubs'},
  {rank:8, suit:'Spades'}
]

console.log(handToPoints(hand)); // {tag:'soft', value:[21, 31]};

Huh, we got the right value, but we know that for Soft, it doesn't make sense to allow the player a choice between 21 and 31 because 31 is always invalid. Even though the answer isn't wrong per se, it does allow the user to do the wrong thing later on, which isn't the greatest.

Let's add one more function, normalize that will check to see if the Points is Soft with a value over 21. If so, we convert to a Hard and throw out the value over 21. Otherwise we return the value (since it's possible for someone to get a Hard score over 21).

function normalize(p:Points): Points {
  if (p.tag === 'soft' && p.value[1] > 21){
    return {tag:'hard', value:p.value[0]}
  }
  return p;
}

// updated addPoints with normalize being used
function addPoints(a:Points, b:Points): Points {
  if (a.tag === 'hard' && b.tag === 'hard'){
    return normalize(addHardAndHard(a, b));
  }
  if (a.tag === 'hard' && b.tag === 'soft'){
    return normalize(addHardAndSoft(a, b));
  }
  if (a.tag === 'soft' && b.tag === 'hard'){
    return normalize(addHardAndSoft(b, a));
  }
  return normalize(addSoftAndSoft(a as Soft, b as Soft));
}

// Note: There's some minor refactoring that we could do here (for example, creating an internal function for handling the add logic and updating `addPoints` to use that function with normalize),
// but will leave that as an exercise to the reader :)

Wrapping Up

In this post, we took a look at using tuples in TypeScript by tackling a portion of the game of Blackjack. Whether it's through using it in types (like we did for Soft) or for destructuring values (like we did in the various addX functions), they can be a handy way of grouping data together for short-term operations.

Interested in knowing more?

If you've enjoyed the above, then you might be interested in my new course (launching Summer 2025) where we build out the game of Blackjack using these concepts in TypeScript. Click here if you're interested in getting an update for when the course goes live!

Tips and Tricks with TypeScript

One of my most recent projects has been tackling how to model the card game, Love Letter. For those who've seen me present my How Functional Programming Made Me a Better Developer talk, you might recall that this was my second project to tackle in F# and that even though I was able to get some of it to work, there were some inconsistency in the rules that I wasn't able to reason about.

While implementing in TypeScript, I came across some cool tricks and thought I'd share some of them here, enjoy!

Swapping two variables

A common enough task in programming, we need to swap two values around. When I was learning C++, I had a memory trick to remember the ordering (think like a zigzag pattern)

1
2
3
4
5
6
7
int a = 100;
int b = 200;

// Note how a starts on the right, then goes left
int temp = a;
a = b;
b = temp;

You can do the same thing in TypeScript, however, you can remove the need for the temp variable by using array destructuring. The idea is that we create an array which contains the two variables to swap. We then assign this array to destructured variables (see below)

1
2
3
4
5
6
let a:number = 100;
let b:number = 200;

// using array destructuring

[a,b] = [b,a]

Drawing a Card

One of the common interactions that happens in the game is that we need to model drawing a card from a deck. As such, we will have a function that looks like the following

1
2
3
4
5
6
7
8
type Card = "Guard" | "Priest" | "Baron" | .... // other options omitted for brevity
type Deck = Card[]

function draw(d:Deck): {card:Card, restOfDeck:Deck} {
  const card = d[0];
  const restOfDeck = d.slice(1);
  return {card, restOfDeck};
}

This absolutely works, however, we can use the rest operator (...) with array destructuring to simplify this code.

1
2
3
4
5
6
7
type Card = "Guard" | "Priest" | "Baron" | .... // other options omitted for brevity
type Deck = Card[]

function draw(d:Deck): {card:Card, restOfDeck:Deck} {
  const [card, ...restOfDeck] = d; // note the rest operator before restOfDeck
  return {card, restOfDeck};
}

This code should be read as assign the first element of the array to a const called card and the rest of the array to a const called restOfDeck.

Drawing Multiple Cards with Object Destructuring

function draw(d:Deck): {card:Card, restOfDeck:Deck} {
  const [card, ...restOfDeck] = d;
  return {card, restOfDeck};
}

function drawMultipleCads(d:Deck, count:number): {cards:Card[], restOfDeck:Deck} {
  let currentDeck = d;
  const cards:Card[] = [];
  for (let i = 0; i < count; i++) {
    const result = draw(currentDeck)
    cards.push(result.card)
    currentDeck = result.restOfDeck
  }
  return {cards, restOfDeck:currentDeck};
}

This works, however, we don't actually care about result, but the specific values card and restOfDeck. Instead of referencing them via property drilling, we can use object destructuring to get their values.

function drawMultipleCads(d:Deck, count:number): {cards:Card[], restOfDeck:Deck} {
  let currentDeck = d;
  const cards:Card[] = [];
  for (let i = 0; i < count; i++) {
    const {card, restOfDeck} = draw(currentDeck) // note using curly braces
    cards.push(card)
    currentDeck = restOfDeck
  }
  return {cards, restOfDeck:currentDeck};
}

Functional Design Patterns - Missing Values

When working in software, a common problem that we run into is what should we do if we can't get a value or compute an answer.

For example, let's say that you go to the library to checkout the latest book from Nathalia Holt, but when you search for it, you realize that they don't have any copies in stock.

How do you handle the absence of value? From an Object Oriented Programming (OOP), you might be able to leverage a reasonable default or the Null Object Pattern. Another approach would be to return to always return a list and if there are no values, then the list would be empty.

No matter the choice you make, we need some way to model the lack of a value. Tony Hoare referred to null as his billion dollar mistake as its inclusion in languages have created countless bugs and errors.

In most functional languages, the idea of null doesn't exist, so how do they handle missing values?

Revisiting Functions

In a previous post, we mention that a function is a mapping between two sets such that every element on the left (first set) is mapped to a single element on the right (the second set). So what happens when we have a mapping that's not a function?

In these cases, we have two solutions to choose from:

  1. Restrict the function input to only be values that can be mapped (e.g., restrict the domain)
  2. Expand the possible values that the function can map to (e.g., expand the range)

When refactoring a mapping, I tend to stick with option 1 because if we can prevent bad data from being created or passed in, that's less error handling that needs to be written and it becomes simpler to reason about. However, there are plenty of times where our type system (or business rules) can't enforce the valid inputs.

Number Math

Let's say that we're working on a "word calculator" where the user can enter a phrase like "one thousand twelve" and it returns "1012" as output. If we think about our input set (domain) and outputs, we would have the following (mapping from string to number).

Mapping from string to number

The issue is that we can't guarantee that the strings the user gives us would represent a number (for example, how would you convert "platypus" to a number?)

Since we can't restrict the input, we have to expand the output. So let's update our output to be number | 'invalid'

Mapping from string to number or 'invalid'

With this change, anytime we call convertToNumber, we'll need to add some handling on what to do if the output is invalid.

Here's some pseudocode of what this mapping could look like

type WordCalculatorResult = number | 'invalid'

function convertToNumber(phrase:string): WorldCalculatorResult {
    if (phrase === 'one') return 1;
    if (phrase === 'two') return 2;
    // ... other logic
    return 'invalid';
}

console.log(convertToNumber('one')); // prints "1";
console.log(convertToNumber('kumquats')) // prints invalid

Leaky Context

So far, so good as we have a function that converts words to numbers. Let's now say that we want to square a number. Writing such a function is straightforward.

1
2
3
function square (x:number): number {
    return x*x;
}

With both convertToNumber and square defined, let's try to combine these together:

Compilation error when combining convertToNumber and Square

This doesn't work because convertToNumber can return invalid which there's no way for the square function to work with that. Due to this limitation, we could rewrite the code to check for invalid before calling square.

1
2
3
4
5
const result = convertToNumber('one');

if (result !== 'invalid') {
    console.log(square(result));
}

This approach will work, but the downside is that we need to add error handling logic in our pipeline now. If this was the only place where it's being used, then it's not too bad. However, if we were using convertToNumber in multiple places, then we have to remember to add this error handling logic. In fact, we would have almost the same if check everywhere.

Let's take a look at a different way of solving this problem by using the Maybe type.

Introducing the Maybe Type

In our original approach, we already had a type called WordCalculatorResult for the computation. Instead of it being a number or invalid, we could instead create a new type, called Maybe that looks like the following:

type Maybe<T> = { label: 'some', value: T } | { label: 'none' }

By leveraging generics and tagged unions, we have a way to represent a missing value for any type. With this new definition, we can rewrite the convertToNumber function to use the Maybe type.

1
2
3
4
5
6
function convertToNumber(phrase:string): Maybe<number> {
    if (phrase === 'one') return {label:'some', value:1};
    if (phrase === 'two') return {label:'some', value:2};
    // other logic
    return {label:'none'};
}

I don't know about you, but typing out {label:'some'} is going to get tiring, so let's create two helper functions, some and none that handle putting the right label if we have a value or not.

1
2
3
4
5
6
7
function some<T>(value:T): Maybe<T> {
    return {label:'some', value};
}

function none<T>(): Maybe<T> {
    return {label:'none'};
}

Which allows us to replace all of the {label:'some', value:x} with some(x) and {label:'none'} with none().

1
2
3
4
5
6
function convertToNumber(phrase:string): Maybe<number> {
    if (phrase === 'one') return some(1);
    if (phrase === 'two') return some(2);
    // other logic
    return none();
}

Now that convertToNumber returns a Maybe, our pipeline would look like the following:

1
2
3
4
5
const result = convertToNumber('one');

if (result.label === 'some') { // checking if we're a Some type
    console.log(square(result.value));
}

Introducing Transformations With Map

Now the problem we're wanting to solve is to get rid of having to do an if check on some in order to call the square function. Given that we're wanting to transform our number into a different number, we can write another function, called map that takes two parameters: the Maybe we want transformed and the function that knows how to transform it. Let's take a look at how to write such a function.

1
2
3
4
5
6
7
function map<T,U>(m:Maybe<T>, f:(t:T)=>U): Maybe<U> {
    if (m.label === 'some') { // Is Maybe a Some?
        const transformedValue = f(m.value); // Grab the value and transform it by calling function f
        return some(transformedValue) // return a new Maybe with the transformed value
    }
    return none(); // Return none since there's no value to transform
}

With map implemented, let's update our code to use it!

const convertResult = convertToNumber('one'); // 
const transformedResult = map(convertResult, square);

if (transformedResult.label === 'some') { // checking if we're a Some type
    console.log(transformedResult.value);
}

/* The above could be simplified as the following
const result = map(convertToNumber('one'), square);
if (result.label === 'some') {
  console.log(result.value)
}
*/

Thanks to map and Maybe, we're able to write two pure functions (convertToNumber and square), glue them together, and delegate our error handling to one spot (instead of having to sprinkle this throughout the codebase).

Calling Side Affects Using Tee

With the addition of map, we've now simplified our if check to only call console.log now (instead of calling the square function then console.log). If we wanted to truly get rid of this if statement, then we need a way to invoke the console.log as a side effect. We don't want to combine console.log into square because square is business rules and we don't want to add side effect logic to our business rules.

Similar to what we did for map, let's add a new function, called tee, that takes in three arguments: a Maybe, a function to call if the Maybe has a value, and then a function to call if Maybe doesn't have a value.

Let's take a look at what an implementation would look like:

/*
 Inputs:
    m: A Maybe to work with
    ifSome: A function that takes in a T (which comes from the Maybe) and returns void
    ifNone: A function that takes no inputs and returns void
 Outputs:
    Returns m so that we can continue chaining calls
*/
function tee<T>(m:Maybe<T>, ifSome:(t:T)=>void, ifNone:()=>void): Maybe<T> {
    if (m.label === 'some') {
        ifSome(m.value);
    } else {
        ifNone();
    }
    return m;
}

With tee implemented, we can update our pipeline to take advantage of it:

const convertResult = convertToNumber('one'); // 
const transformedResult = map(convertResult, square);

const ifSome = (t:number) => console.log(`Result is ${t}`);
const ifNone = () => console.log('Failed to calculate result.');

tee(transformedResult, ifSome, ifNone);

// if we wanted to get rid of the temp values, we could do the following
// const result = tee(map(convertToNumber('one'), square)), ifSome, ifNone);

Cleaning Up by using an Object

We've now centralized the if logic to the functions that work on Maybes which is helpful, but as we're already seeing, the code can be hard to parse as you need to read from the outside in (which is very Clojure or Scheme in nature).

Instead of building up functions this way, we can instead create a TypeScript class and use a fluent interface pattern by doing the following:

export class Maybe<T>
{
  private constructor(private readonly value:T|undefined=undefined){}
  map<U>(f:(t:T)=>U): Maybe<U> {
    return this.value !== undefined ? Maybe.some(f(this.value)) : Maybe.none();
  }
  tee(ifSome:(t:T)=>void, ifNone:()=>void) {
    if (this.value !== undefined) {
      ifSome(this.value);
    } else {
      ifNone();
    }
    return this;
  }
  static some<T>(value:T): Maybe<T> {
    return new Maybe(value);
  }
  static none<T>(): Maybe<T> {
    return new Maybe();
  }
}
// Defined in index.ts
function convertToNumber(phrase:string): Maybe<number> {
    if (phrase === 'one') return Maybe.some(1);
    if (phrase === 'two') return Maybe.some(2);
    // other logic
    return Maybe.none();
}

function square(x:number): number {
    return x*x;
}

// By using the fluent interface pattern, we can chain methods together and have this code read
// more naturally
convertToNumber("one")
  .map(square)
  .tee(
    (v)=> `Answer is ${v}`, 
    () => "Unable to calculate result"
  );

Wrapping Up

When writing software, a common problem we run into is what to do when we can't calculate a value or a result. In this post, we looked at techniques to restrict the function's input or expand their output to handle scenarios. From there, we looked into a type called Maybe and how it can represent the absence of value, but still provide a way to remove having to explicitly write error handling code by consolidating the checks into a map call. Lastly, we look into taking the various functions that we wrote and combine them into a formal Maybe class that allows us to leverage a fluent interface and chain our interactions together.

Functional Design Patterns - Reduce and Monoids

When learning about a loops, a common exercise is to take an array of elements and calculate some value based on them. For example, let's say that we have an array of numbers and we want to find their sum. One approach would be the following:

1
2
3
4
5
const values = [1, 2, 3, 4, 5];
let total = 0;
for (const val in values) {
    total += val;
}

This code works and we'll get the right answer, however, there's a pattern sitting here. If you find yourself writing code that has this shape (some initial value and some logic in the loop), then you have a reducer in disguise.

Let's convert this code to use the reduce function instead.

const values = [1, 2, 3, 4, 5];
const total = values.reduce((acc, curr) => acc + curr, 0);

The reduce function takes two parameters, the reducer and the initial value. For the initial value, this is the value that the result should be if the array is empty. Since we're adding numbers together, zero makes the most sense.

The reducer, on the other hand, says that if you give me the accumulated result so far (called acc in the example) and an element from the array (called curr in the above example), what's the new accumulation?

Reduce is an extremely powerful tool (in fact, I give a presentation where we rewrite some of C#'s LINQ operators as reduce).

But there's another pattern sitting here. If you find that the initial value and elements of the array have the same type, you most likely have a monoid.

Intro to Monoids

The concept of Monoid comes from the field of category theory where a Monoid contains three things

  1. A set of values (you can think about this as a type)
  2. Some binary operation (e.g., a function that takes two inputs and returns a single output of said type)
  3. Some identity element such that if you pass the id to the binary operation, you get the other element back.

There's a lot of theory sitting here, so let's put it in more concrete terms.

If we have a Monoid over the a type A, then the following must be true:

1
2
3
4
5
6
7
8
function operation<A>(x:A, y:A): A 
{
    // logic for operation
}
const identity: A = // some value of type A
operation(x, identity) === operation(identity, x) === x

operation(x, operation(y, z)) === operation(operation(x, y), z)

Here's how we would define such a thing in TypeScript.

1
2
3
4
export interface Monoid<A>{
    operation: (x:A, y:A)=> A;
    identity:A;
}

Exploring Total

With more theory in place, let's apply it to our running total from before. It turns out that addition forms a Monoid over positive numbers with the following:

1
2
3
4
const additionOverNumber: Monoid<number> = {
    identity: 0,
    operation: (a:number, b:number) => a+b;
}

Wait a minute... This looks like what the reduce function needed from before!

In the case where we have a Monoid, we have a way of reducing an array to a single value for free because of the properties that Monoid gives us.

Exploring Booleans

Thankfully, we're not limited to just numbers. For example, let's take a look at booleans with the && and || operators.

In the case of &&, that's our operation, so now we need to find the identity element. In other words, what value must id be if the following statements are true?

1
2
3
const id: boolean = ...
id && true === true
id && false === false

Since id has to be a boolean, the answer is true. Therefore, we can define our Monoid like so

1
2
3
4
const AndOverBoolean: Monoid<boolean> = {
    identity: true,
    operation: (a:boolean, b:boolean) => a && b
}

With this monoid defined, let's put it to use. Let's say that we wanted to check if every number in a list is even. We could write the following:

1
2
3
const values = [1, 2, 3, 4, 5];

const areAllEven = values.map(x=>x%2===0).reduce(AndOverBoolean.operation, AndOverBoolean.identity);

Huh, that looks an awful lot like how we could have used every.

const values = [1, 2, 3, 4, 5];
const areAllEven = values.every(x=>x%2===0);

Let's take a look at the || monoid now. We have the operation, but we now we need to find the identity. In other words, what value must id be if the following statements are true?

1
2
3
const id: boolean = ...;
id || true === true
id || false === false

Since id has to be a boolean, the answer is false. Therefore, we can define our monoid as such.

1
2
3
4
const OrOverBoolean: Monoid<boolean> = {
    identity: false,
    operation: (x:boolean, y:boolean) => x || y
} 

With this monoid defined, let's put it to use. Let's say that we wanted to check if some number in a list is even. We could write the following:

1
2
3
const values = [1, 2, 3, 4, 5];

const areAllEven = values.map(x=>x%2===0).reduce(OrOverBoolean.operation, OrOverBoolean.identity);

Similar to the AndOverBoolean, this looks very similar to the code we would have written if we had leveraged some.

1
2
3
const values = [1, 2, 3, 4, 5];

const isAnyEven = values.some(x => x%2 === 0);

Wrapping Up

When working with arrays of items, it's common to need to reduce the list down to a single element. You can start with a for loop and then refactor to using the reduce function. If the types are all the same, then it's likely that you also have a monoid, which can give you stronger guarantees about your code.

Today I Learned - Leveraging Mock Names with Jest

I was working through the Mars Rover kata the other day and found myself in a predicament when trying to test one of the functions, the convertCommandToAction function.

The idea behind the function is that based on the Command you pass in, it'll return the right function to call. The code looks something like this.

type Command = 'MoveForward' | 'MoveBackward' | 'TurnLeft' | 'TurnRight' | 'Quit'
type Action = (r:Rover):Rover;

const moveForward:Action = (r:Rover):Rover => {
  // business rules
}
const moveBackward:Action = (r:Rover): Rover => {
  // business rules
}
const turnLeft:Action = (r:Rover):Rover => {
  // business rules
}
const turnRight:Action = (r:Rover): Rover => {
  // business rules
}
const quit:Action = (r:Rover):Rover => {
  // business rules
}

// Function that I'm wanting to write tests against.
function convertCommandToAction(c:Command): Action {
  switch (c) {
    case 'MoveForward': return moveForward;
    case 'MoveBackward': return moveBackward;
    case 'TurnLeft': return turnLeft;
    case 'TurnRight': return turnRight;
    case 'Quit': return quit;
  }
}

I'm able to write tests across all the other functions easily enough, but for the convertCommandToAction, I needed some way to know which function is being returned.

Since I don't want the real functions to be used, my mind went to leveraging Jest and mocking out the module that the actions were defined in, yielding the following test setup.

import { Command } from "./models";
import { convertCommandToAction, convertStringToCommand } from "./parsers";

jest.mock("./actions", () => ({
  moveForward: jest.fn(),
  moveBackward: jest.fn(),
  turnLeft: jest.fn(),
  turnRight: jest.fn(),
  quit: jest.fn(),
}));

describe("When converting a Command to an Action", () => {
  it("and the command is MoveForward, then the right action is returned", () => {
    const result = convertCommandToAction("MoveForward");

    // What should my expect be?
    expect(result);
  });
});

One approach that I have used in the past is jest's ability to test if a function is a mocked function, however, that approach doesn't work here because all of the functions are being mocked out. Meaning, that my test would pass, but if I returned moveBackward instead of moveForward, my test would still pass (but now for the wrong reason). I need a way to know which function was being returned.

Doing some digging, I found that the jest.fn() has a way of setting a name for a mock by leveraging the mockName function. This in turn allowed me to change my setup to look like this.

1
2
3
4
5
6
7
jest.mock("./actions", () => ({
  moveForward: jest.fn().mockName('moveForward'),
  moveBackward: jest.fn().mockName('moveBackward'),
  turnLeft: jest.fn().mockName('turnLeft'),
  turnRight: jest.fn().mockName('turnRight'),
  quit: jest.fn().mockName('quit'),
}));
Note: It turns out that the mockName function is part of a fluent interface, which allows it to return a jest.Mock as the result of the mockName call

With my setup updated, my tests can now check that the result has the right mockName.

1
2
3
4
5
6
7
8
9
describe("When converting a Command to an Action", () => {
  it("and the command is MoveForward, then the right action is returned", () => {

    // have to convert result as a jest.Mock to make TypeScript happy
    const result = convertCommandToAction("MoveForward") as unknown as Jest.Mock;

    expect(result.getMockName()).toBe("moveForward");
  });
});

Wrapping Up

If you find yourself writing functions that return other function (i.e., leveraging functional programming concepts), then you check out using mockName for keeping track of which functions are being returned.

Today I Learned: Validating Data in Zod

Validating input. You've got to do it, otherwise, you're going to be processing garbage, and that never goes well, right?

Whether it's through the front-end (via a form) or through the back-end (via an API call), it's important to make sure that the data we're processing is valid.

Coming from a C# background, I was used to ASP.NET Web Api's ability to create a class and then use the FromBody attribute for the appropriate route to ensure the data is good. By using this approach, ASP.NET will reject requests automatically that don't fit the data contract.

However, picking up JavaScript and TypeScript, that's not the case. At first, this surprised me because I figured that this would automatically happen when using libraries like Express or Nest.js. Thinking more about it, though, it shouldn't have surprised me. ASP.NET can catch those issues because it's a statically typed/ran language. JavaScript isn't and since TypeScript types are removed during the compilation phase, neither is statically typed at runtime.

When writing validations, I find zod to be a delightful library to leverage. There are a ton of useful built-in options, you can create your own validators (which you can then compose!) and you can infer models based off of your validations.

Building the Amazin' Bookstore

To demonstrate some of the cool things that you can do with Zod, let's pretend that we're building out a new POST endpoint for creating a new book. After talking to the business, we determine that the payload for a new book should look like this:

1
2
3
4
5
6
// A valid book will have the following
// - A non-empty title
// - A numeric price (can't be negative or zero)
// - A genre from a list of possibilities (mystery, fantasy, history are examples, platypus would not be valid)
// - An ISBN which must be in a particular format
// - A valid author which must have a first name, a last name, and an optional middle name

What's in a Name?

Since the Book type needs a valid Author, let's build that out first:

1
2
3
4
5
import {z} from "zod";

export const AuthorSchema = z.object({

});

Since Author will need to be an object, we'll use z.object to signify that. Right off the bat, this prevents a string, number, or other primitive types from being accepted.

1
2
3
AuthorSchema.safeParse("someString"); // will result in a failure
AuthorSchema.safeParse(42); // will result in a failure
AuthorSchema.safeParse({}); // will result in success!

This is a great start, but we know that Author has some required properties (like a first name), so let's implement that by using z.string()

1
2
3
export const AuthorSchema = z.object({
    firstName: z.string()
});

With this change, let's take a look at our schema validation

1
2
3
AuthorSchema.safeParse({}); // fails because no firstName property
AuthorSchema.safeParse({firstName:42}); // fails because firstName is not a string
AuthorSchema.safeParse({firstName: "Cameron"}); // succeeds because firstName is present and a string

However, there's one problem with our validation. We would allow an empty firstName

AuthorSchema.safeParse({firstName:""}); // succeeds, but should have failed :(

To make our validation stronger, we can update our firstName property to have a minimum length of 1 like so.

1
2
3
export const AuthorSchema = z.object({
    firstName: z.string().min(1)
});

Finally, we have a way to enforce that an author has a non-empty firstName!. Looking at the requirements, it seems like lastName is going to be similar, so let's update our AuthorSchema to include lastName.

1
2
3
4
export const AuthorSchema = z.object({
    firstName: z.string().min(1),
    lastName: z.string().min(1)
});

Hmmm, it looks like we have the same concept in multiple places, the idea of a non empty string. Let's refactor that to its own schema.

1
2
3
4
5
6
export const NonEmptyStringSchema = z.string().min(1);

export const AuthorSchema = z.object({
    firstName: NonEmptyStringSchema,
    lastName: NonEmptyStringSchema
});

Nice! We're almost done with Author, we need to implement middleName. Unlike the other properties, an author may not have a middle name. In this case, we're going to leverage the optional function from zod to signify that as so.

1
2
3
4
5
6
7
8
9
export const NonEmptyStringSchema = z.string().min(1);

export const AuthorSchema = z.object({
    firstName: NonEmptyStringSchema,
    lastName: NonEmptyStringSchema,
    // This would read that middleName may or not may be present. 
    // If it is, then it must be a string (could be empty)
    middleName: z.string().optional(), 
});

With the implementation of AuthorSchema, we can start working on the BookSchema.

Judging a Book By It's Cover

Since we have AuthorSchema, we can use that as our start as so:

1
2
3
export const BookSchema = z.object({
    author: AuthorSchema
});

We know that a book must have a non-empty title, so let's add that to our definition. Since it's a string that must have at least one character, we can reuse the NonEmptyStringSchema definition from before.

1
2
3
4
export const BookSchema = z.object({
    author: AuthorSchema,
    title: NonEmptyStringSchema
});

Putting a Price on Knowledge

With title in place, let's leave the string theory alone for a bit and look at numbers. In order for the bookstore to function, we've got sell books for some price. Let's use z.number() and add a price property.

1
2
3
4
5
export const BookSchema = z.object({
    author: AuthorSchema,
    title: NonEmptyStringSchema,
    price: z.number()
});

This works, however, z.number() will accept any number, which includes numbers like 0 and -5. While those values would be great for the customer, we can't run our business that way. So let's update our price to only include positive numbers, which can be accomplished by leveraging the positive function.

1
2
3
4
5
export const BookSchema = z.object({
    author: AuthorSchema,
    title: NonEmptyStringSchema,
    price: z.number().positive()
});

With price done, let's look at validating the genre.

Would You Say It's a Mystery or History?

Up to this point, all of our properties have been straightforward (simple strings and numbers). However, with genre, things get more complicated because it can only be one of a particular set of values. Thankfully, we can define a GenreSchema by using z.enum() like so.

export const GenreSchema = z.enum(["Fantasy", "History", "Mystery"]);

With this definition, a valid genre can only be fantasy, history, or mystery. Let's update our book definition to use this new schema.

1
2
3
4
5
6
export const BookSchema = z.object({
    author: AuthorSchema,
    title: NonEmptyStringSchema,
    price: z.number().positive(),
    genre: GenreSchema
});

Now, someone can't POST a book with a genre of "platypus" (though I'd enjoy reading such a book).

ID Please

Last, let's take a look at implementing the isbn property. This is interesting because ISBNs can be in one of two shapes: ISBN-10 (for books pre-2007) and ISBN-13 (all other books).

To make this problem easier, let's focus on the ISBN-10 format for now. A valid value will be in the form of #-###-#####-# (where # is a number). Now, you can take this a whole lot further, but we'll keep on the format.

Now, even though zod has built-in validators for emails, ips, and urls, there's not a built-in one for ISBNs. In these cases, we can use .refine to add our logic. But this is a good use case for a basic regular expression. Using regex101 as a guide, we end up with the following expression and schema for the ISBN.

const isbn10Regex = /^\d-\d{3}-\d{5}-\d/;
export const Isbn10Schema = z.string().regex(isbn10Regex);

Building onto that, an ISBN-13 is in a similar format, but has the form of ###-#-##-######-#. By tweaking our regex, we end up with the following:

const isbn13Regex = /^\d{3}-\d-\d{2}-\d{6}-\d/;
export const Isbn13Schema = z.string().regex(isbn13Regex);

When modeling types in TypeScript, I'd like to be able to do something like the following as this makes it clear that an ISBN can in one of these two shapes.

1
2
3
type Isbn10 = string;
type Isbn13 = string;
type Isbn = Isbn10 | Isbn13;

While we can't use the | operator, we can use the .or function from zod to have the following

1
2
3
4
5
6
const isbn10Regex = /^\d-\d{3}-\d{5}-\d/;
export const Isbn10Schema = z.string().regex(isbn10Regex);
const isbn13Regex = /^\d{3}-\d-\d{2}-\d{6}-\d/;
export const Isbn13Schema = z.string().regex(isbn13Regex);

export const IsbnSchema = Isbn10Schema.or(Isbn13Schema);

With the IsbnSchema in place, let's add it to BookSchema

1
2
3
4
5
6
7
export const BookSchema = z.object({
    author: AuthorSchema,
    title: NonEmptyStringSchema,
    price: z.number().positive(),
    genre: GenreSchema
    isbn: IsbnSchema
});

Getting Models for Free

Lastly, one of the cooler functions that zod supports is infer where if you pass it a schema, it can build out a type for you to use in your application.

export const BookSchema = z.object({
    author: AuthorSchema,
    title: NonEmptyStringSchema,
    price: z.number().positive(),
    genre: GenreSchema
    isbn: IsbnSchema
});

// TypeScript knows that Book must have an author (which has a firstName, lastName, and maybe a middleName)
// a title (string), a price (number), a genre (string), and an isbn (string).
export type Book = z.infer<typeof BookSchema>; 

Full Solution

Here's what the full solution looks like

const NonEmptyStringSchema = z.string().min(1);
const GenreSchema = z.enum(["Fantasy", "History", "Mystery"]);

export const AuthorSchema = z.object({
  firstName: NonEmptyString,
  lastName: NonEmptyString,
  middleName: z.string().optional(),
});

export const Isbn10Schema = z.string().regex(/^\d-\d{2}-\d{6}-\d/);
export const Isbn13Schema = z.string().regex(/^\d{3}-\d-\d{2}-\d{6}-\d/);
export const IsbnSchema = Isbn10Schema.or(Isbn13Schema);

export const BookSchema = z.object({
  title: NonEmptyString,
  author: AuthorSchema,
  price: z.number().positive(),
  genre: GenreSchema,
  isbn: IsbnSchema,
});

export type Book = z.infer<typeof BookSchema>;

With these schemas and models defined, we can leverage the safeParse function to see if our input is valid.

describe('when validating a book', () => {
    it("and the author is missing, then it's not valid", () => {
        const input = {title:"best book", price:200, genre:"History", isbn:"1-23-456789-0"}

        const result = BookSchema.safeParse(input);

        expect(result.success).toBe(false);
    });
    it("and all the fields are valid, then the book is valid", () => {
        const input = {
            title:"best book", 
            price:200, 
            genre:"History", 
            isbn:"1-23-456789-0", 
            author: {
                firstName:"Super", 
                middleName:"Cool", 
                lastName:"Author"
            }
        };

        const result = BookSchema.safeParse(input);

        expect(result.success).toBe(true);
        const book:Book = result.data as Book;
        // now we can start using properties from book
        expect(book.title).toBe("best book");
    });
});

Functional Foundations - Functions

When leveraging functional programming, you're not going to go far without functions. It's literally in the name.

In this post, let's take a deeper look at what functions are and some of the benefits we gain.

What is a Function?

When we talk about functions, we're not talking about a programming construct (like the function keyword), but instead we're talking about functions from mathematics.

As such, a function is a mapping from two sets such that for every element of the first set, it maps to a single element in the second set.

Words are cool, but pictures are better. So let's look at the mapping for the square function.

Mapping for the Square Function

In this example, we have an arrow coming from an element on the left where it maps to an element on the right. To read this image, we have a mapping called Square that maps all possible numbers to the set of positive numbers. So -3 maps to 9 (-3-3), 2 maps to 4 (22), so on and so forth.

To check if our mapping is a function, we need to check that every element on the left is mapped to a single element on the right. If so, then we've got a function!

Sounds easy, right? Let's take a look at a mapping that isn't a function.

Love in the Air?

When working with dates, it's common to figure out how many days are in the month. Not only does this help with billable days, but it also makes sure that we don't try to send invoices on May 32nd.

So let's take a look at a mapping from month to the number of days it has.

Broken Mapping for the Days In Month Function

Looking at the mapping, we can tell that January, March, May map to 31, April and June both map to 30. But take a look at February. It's got two arrows coming out of it, one to 28 and the other to 29. Because there are two arrows coming out, this mapping isn't a function. Let's try to implement this mapping in TypeScript.

type Month = "Jan" | "Feb" | "Mar" | "Apr"
           | "May" | "Jun" | "Jul" | "Aug"
           |"Sept" | "Oct" | "Nov" | "Dec";

type DaysInMonth = 28 | 29 | 30 | 31;

function getDaysInMonth(month: Month): DaysInMonth {
  switch (month) {
    case "Jan":
    case "Mar":
    case "May":
    case "Jul":
    case "Oct":
    case "Dec":
      return 31;

    case "Feb":
      // what should this be?

    case "Apr":
    case "Jun":
    case "Aug":
    case "Sept":
    case "Nov":
      return 30;
  }
}

We can't return 28 all the time (we'd be wrong 25% of the time) and we can't return 29 all the time (as we'd be wrong 75% of the time). So how do we know? We need to know something about the year. One approach would be to check if the current year is a leap year (algorithm).

function isLeapYear(): boolean {
  const year = new Date().getFullYear();
  if (year % 400 === 0) return true;
  if (year % 100 === 0) return false;
  if (year % 4 === 0) return true;
  return false;
}

// Updated switch
case 'Feb':
  return isLeapYear() ? 29 : 28;

The problem with this approach is that the determination of what to return isn't from the function's inputs, but outside state (in this case, time). So while this "works", you can get bit when you have tests that start failing when the calendar flips over because it assumed that February always had 28 days.

If we look at the type signature of isLeapYear, we can see that it takes in no inputs, but returns a boolean. How can that be possible except if it always returned a constant value? This is a clue that isLeapYear is not a function.

The better approach is to change our mapping to instead of taking just a month name, it takes two arguments, a monthName and year.

Fixed Mapping For Days In Month

With this new mapping, our implementation would look like the following:

function isLeapYear(year:number): boolean {
  if (year % 400 === 0) return true;
  if (year % 100 === 0) return false;
  if (year % 4 === 0) return true;
  return false;
}

function getDaysInMonth(month: Month, year:number): DaysInMonth {
  const isLeap = isLeapYear(year);
  switch (month) {
    case "Jan":
    case "Mar":
    case "May":
    case "Jul":
    case "Oct":
    case "Dec":
      return 31;

    case "Feb":
      return isLeap ? 29 : 28

    case "Apr":
    case "Jun":
    case "Aug":
    case "Sept":
    case "Nov":
      return 30;
  }
}

Benefits of Functions

Now that we've covered what functions are and aren't, let's cover some of the reasons why we prefer functions for our logic.

First, mappings help us make sure that we've covered all our bases. We saw in the getDaysInMonth function we found a bug for when the month was February. Mappings can also be great conversation tools with non-engineers as they're intuitive to understand and to explain.

Second, functions are simple to test. Since the result is based solely on inputs, they are great candidates for unit testing and require little to no mocking to write them. I don't know about you, but I like simple test cases that help us build confidence that our application is working as intended.

Third, we can combine functions to make bigger functions using composition. At a high level, composition says that if we have two functions f and g, we can write a new function, h which takes the output of f and feeds it as the input for g.

Sounds theoretical, but let's take a look at a real example.

In the Mars Rover kata, we end up building a basic console application that takes the input from the user (a string) and will need to convert it to the action that the rover takes.

In code, the logic looks like the following:

1
2
3
let rover:Rover = {x:0, y:0, direction:'North'};
const action = input.split('').map(convertStringToCommand).map(convertCommandToAction);
rover = action(rover);

The annoying part is that we're iterating the list twice (once for each map call), and it'd be nice to get it down to a single iteration. This is where composition helps.

When we're running the maps back-to-back, we're accomplish the following workflow

Input to Command to Action Mapping

Because each mapping is a function, we can compose the two into a new function, stringToActionConverter.

1
2
3
4
5
6
// using our f and g naming from earlier, convertString is f, convertCommand is g
const stringToActionConverter = (s:string)=>convertCommandToAction(convertStringToCommand(s));

let rover = {x:0, y:0, direction:'North'}
const action = input.split('').map(stringToActionConverter);
rover = action(rover);

Why Not Function All the Things?

Functions can greatly simplify our mental model as we don't have to keep track of state or other side effects. However, our applications typically deal with side affects (getting input from users, reading from files, interacting with databases) in order to do something useful. Because of this limitation, we strive to put all of our business rules into functions and keep the parts that interact with state as dumb as possible (that way we don't have to troubleshoot as much).

What I've found is that when working with applications, you end up with a workflow where you have input come in, gets processed, and then the result gets outputted.

Here's what an example workflow would look like

// Logic to determine the 'FizzBuzziness' of a number
function determineFizzBuzz(input:number): string {
  if (input % 15 === 0) return 'FizzBuzz';
  if (input % 3 === 0) return 'Fizz';
  if (input % 5 === 0) return 'Buzz';
  return `${input}`;
}

function workflow(): void {
  // Input Boundary
  var prompt = require('prompt-sync')();
  const input = prompt();

  // Business Rules
  const result = (+input) ? `${input} FizzBuzz value is ${determineFizzBuzz(+input)}` : `Invalid input`;

  // Output boundary
  console.log(result);
}

What's Next?

Now that we have a rough understanding of functions, we can start exploring what happens when things go wrong. For example, could there have been a cleaner way of implementing the business rules of our workflow?

Today I Learned: Destructure Objects in Function Signatures

When modeling types, one thing to keep in mind is to not leverage primitive types for your domain. This comes up when we use a primitive type (like a string) to represent core domain concepts (like a Social Security Number or a Phone Number).

Here's an example where it can become problematic:

// Definition for Customer
type Customer = {
  firstName: string,
  lastName: string,
  email: string,
  phoneNumber: string
}

// Function to send an email to customer about a new sale
async function sendEmailToCustomer(c:Customer): Promise<void> {
  const content = "Look at these deals!";

  // Uh oh, we're trying to send an email to a phone number...
  await sendEmail(c.phoneNumber, content);
}

async function sendEmail(email:string, content:string): Promise<void> {
  // logic to send email
}

There's a bug in this code, where we're trying to send an email to a phone number. Unfortunately, this code type checks and compiles, so we have to lean on other techniques (automated testing or code reviews) to discover the bug.

Since it's better to find issues earlier in the process, we can make this a compilation error by introducing a new type for Email since not all strings should be treated equally.

One approach we can do is to create a tagged union like the following:

1
2
3
4
type Email = {
  label:"Email",
  value:string
}

With this in place, we can change our sendEmail function to leverage the new Email type.

1
2
3
function sendEmail(email:Email, content:string): Promise<void> {
  // logic to send email
}

Now, when we get a compilation error when we try passing in a phoneNumber.

Compilation error that we can't pass a string to an email

One downside to this approach is that if you want to get the value from the Email type, you need to access it's value property. This can be a bit hard to read and keep track of.

1
2
3
4
function sendEmail(email:Email, content:string): Promise<void> {
  const address = email.value;
  // logic to send email
}

Leveraging Object Destructuring in Functions

One technique to avoid this is to use destructuring to get the individual properties. This allows us to "throw away" some properties and hold onto the ones we care about. For example, let's say that we wanted only the phoneNumber from a Customer. We could get that with an assignment like the following:

1
2
3
4
5
6
7
8
const customer: Customer = {
  firstName: "Cameron",
  lastName: "Presley",
  phoneNumber: "555-5555",
  email: {label:"Email", value:"Cameron@domain.com"}
}

const {phoneNumber} = customer; // phoneNumber will be "555-555"

This works fine for assignments, but it'd be nice to have this at a function level. Thankfully, we can do that like so:

1
2
3
4
5
// value is the property from Email, we don't have the label to deal with
function sendEmail({value}:Email, content:string): Promise<void> {
  const address = value; // note that we don't have to do .value here
  // logic to send email
}

If you find yourself using domain types like this, then this is a handy tool to have in your toolbox.

Today I Learned: LibYear

When writing software, it's difficult (if not impossible) to write everything from scratch. We're either using a framework or various third-party libraries to make our code work.

On one hand, this is a major win for the community because we're not all having to solve the same problem over and over again. Could you imagine having to implement your own authentication framework (on second thought...)

However, this power comes at a cost. These libraries aren't free as in beer, but more like puppies. So if we're going to take in the library, then we need to make sure that our dependencies are up-to-date with the latest and greatest. There are new features, bug fixes, and security patches occurring all the time and the longer we let a library drift, the more painful it can be to upgrade.

If the library is leveraging semantic versioning, then we can take a guess on the likelihood of a breaking change base on which number (Major.Minor.Maintenance) has changed.

  • Major - We've made a breaking change that's not backward compatible. Your code may not work anymore.
  • Minor - We've added added new features that you might be interested in or made other changes that are backwards compatible.
  • Maintenance - We've fixed some bugs, sorry about that!

Keeping Up With Dependencies

For libraries that have known vulnerabilities, you can leverage tools like GitHub's Dependabot to auto-create pull requests that will upgrade those dependencies for you. Even though the tool might be "noisy", this is a great way to take an active role in keeping libraries up to date.

However, this approach only works for vulnerabilities, what about libraries that are just out-of-date? There's a cost/benefit of upgrading where the longer you go between the upgrades, the riskier the upgrade will be.

In the JavaScript world, we know that dependencies are listed in the package.json file with minimum versions and the package-lock.json file states the exact versions to use.

Using LibYear

I was working with one of my colleagues the other day and he referred me to a library called LibYear that will check your package.json and lock file to determine how much "drift" that you have between your dependencies.

Under the hood, it's combining the npm outdated and npm view <package> commands to determine the drift.

What I like about this tool is that you can use this as a part of a "software health" report for your codebase.

As engineers, we get pressure to ship features and hit delivery dates, but it's our responsibility to make sure that our codebase is in good shape (however we defined that term). I think this library is a good way for us to capture a data point about software health which then allows the team to make a decision on whether we should update our libraries now (or defer).

The nice thing about the LibYear package is that it lends itself to be ran in a pipeline and then you could take those results and post them somewhere. For example, maybe you could write your own automation bot that could post the stats in your Slack or Teams chat.

It looks like there's already a GitHub Action for running this tool today, so you could start there as a basis.