… or “FP to the Max with TypeScript”.

Inspired by amazing FP to the Max video by John A De Goes here I’m going to replicate the result John obtained using TypeScript and try get as close as possible to build a simple game application using generic functional programming style.

Challenges

First, there are some challanges to solve before we can start. Scala has a richer type system and in the example there are some parts of the puzzle missing in the TypeScript:

  • Generic higher kinded types: Ex[F[_]] (http://adriaanm.github.io/files/higher.pdf)
  • Implicits or Extension functions - which allows to enrich data types with new functionality from outside
  • for comprehension syntax

Generic higher kinded types

Unfortunatly TypeScript doesn’t support higher kinded types natively yet, but meanwhile the community seems to converge to use it’s lightweight implementation:

For example a Functor type class will looks like this:

import { HK } from "funland"

export interface Functor<_URI> {
  map<A, B>(f: (a: A) => B, fa: HK<_URI, A>): HK<_URI, B>
}

HK<_URI, _> here is equivalent to F[_] in scala, where _URI type parameter is used to identify type F.

One drawback of this construct is that you can’t easily go back to concrete type when needed, but going concrete sounds like an implementation detail and will look how to workaround it later.

So let’s call challenge #1 - solved.

Implicits

Scalas implicits allows its users to achieve nicer syntax for their DSLs by adding extra functionality to existing data types (adhoc polymorphism), similar to extension methods from kotlin, swift, or you can bring dependent data (ex. type classes) into your local context without passing them explicitly.

For TypeScript I’m not aware of any functionality that can help us build similar functionality, so we will pass our type classes explicitly through function arguments.

for comprehension syntax

Not much here also so will use a helper function to chain operations sequence.

The App

Our goal is to build a game application. It will generate a random number then will ask user to guess it, user will enter his guess then program will say if that was the rigth number or not. When round ends - user may choose to continue the game or stop.

Full source code is here: lostintime/fp-to-the-max.

Domains and Languages

For the app specifications above - first we will split our app business domain into smaller ones and define basic languages for them.

A domain language will consist of:

  • Custom data types, if needed (language primitive types may be enough)
  • An interface defining language functions, using generic higher kinded types for non-domain data structures

Spoiler: The way we will define the DSLs is named Tagless Final, here are few links to read more:

Console

In order to interact with user we will use comand line interface to read user’s input and write messages to user, so console domain language is defined as an interface with 2 functions:

export interface Console<F> {

  /**
   * Displays a message to user
   */
  writeLine(line: string): HK<F, void>

  /**
   * Reads a line from user's input
   */
  readLine(): HK<F, string>
}

src: console.ts

Random

App will need to generate random numbers for user to guess:

export interface Random<F> {

  /**
   * Generates a random integer number within given closed range
   */
  randomInt(min: number, max: number): HK<F, number>
}

src: random.ts

Strings and Numbers

So far we can read and write string lines from/to user and generate random numbers, but ito compare user guess with generated value we will have to parse an integer number from user’s string input (or transform generated integer number to a string):

export interface Numbers<F> {

  /**
   * Parse an integer from a string
   * NOTE: Failures will be encoded within HKT, for simplicity
   */
  parseInt(num: string): HK<F, number>
}

src: numbers.ts

Program

In order to be able to build more complex programs - we’re missing some glue, let’s put them together within new Program DSL:

export interface Program<F> {
  /**
   * Lift a constant A
   */
  pure<A>(a: A): HK<F, A>

  /**
   * Chain 2 operations
   */
  chain<A, B>(f: (a: A) => HK<F, B>, fa: HK<F, A>): HK<F, B>
}

pure function will wrap given value into our box type, chain - will apply a transformation over given boxed value A.

Game Time!

Now we’re ready to build our game application using the languages we defined above. It is defined as a function taking as argument a type with all the DSLs needed for it’s implementation.

Hello World

When game starts - we will show a “Welcome” message to the user:

/**
 * Game DSL is composed from all languages needed to build it
 * At this level we only need to print a message to console
 */
export type GameDSL<F> = Console<F>

/**
 * Game Application:
 *  1. Say Hello
 */
export const game = <F>(dsl: GameDSL<F>): HK<F, void> => {
  const { writeLine } = dsl

  return writeLine("Hello & welcome to our game (press CTRL/CMD+C to exit)")
}

Play npm start -s:

Game Round

After welcome message - a game round starts, we will define it as separate function (application) which:

  • Displays round rules message to user;
  • Generates a random number;
  • Asks user to guess and enter the number;
  • Compare the guess with generated number and show round results.
/**
 * The type composes all the languages neede to build a single round game application
 */
export type GameRoundDSL<F> = Program<F> & Console<F> & Numbers<F> & Random<F>

/**
 * Single round application
 */
const gameRound = <F>(dsl: GameRoundDSL<F>): HK<F, void> => {
  const { chain, readLine, writeLine, parseInt, randomInt } = dsl

  const intro = writeLine("Please enter a number from 1 to 5")

  const genInt = () => randomInt(1, 5)

  const readGuess = chain(parseInt, readLine())

  const checkGuess = (num: number) => (guess: number) =>
    guess === num
      ? writeLine("You Win!")
      : writeLine(`Wrong! The right number was ${num}`)

  return chain(
    num => chain(
      checkGuess(num),
      readGuess
    ),
    chain(genInt, intro)
  )
}

/**
 * Game Application:
 *  1. Say Hello
 *  2. Run game round
 */
export const game = <F>(dsl: GameDSL<F>): HK<F, void> => {
  const { chain, writeLine } = dsl

  return chain(
    () => gameRound(dsl),
    writeLine("Hello & welcome to our game (press CTRL/CMD+C to exit)")
  )
}

Play npm start -s:

Loop

And finally - we don’t want our game to exit after single round so will write game loop to continue with user’s confirmation:

/**
 * Recursive game loop: plays a round then asks user to continue
 */
const gameLoop = <F>(dsl: GameDSL<F>): HK<F, void> => {
  const { pure, chain, readLine, writeLine } = dsl

  const checkContinue = chain(
    answer => pure(
      answer.toLowerCase() === "y" || answer.trim() === "" ? true : false
    ),
    chain(
      readLine,
      writeLine("Do you want to play again? [Y/n]:")
    )
  )

  return chain(
    () => chain(
      // The recursive call is not stack safe!
      go => go ? gameLoop(dsl) : pure(undefined),
      checkContinue
    ),
    gameRound(dsl)
  )
}

/**
 * Game Application:
 *  1. Say Hello
 *  2. Run game loop
 *  3. Done
 */
export const game = <F>(dsl: GameDSL<F>): HK<F, void> => {
  const { chain, writeLine } = dsl
  const intro = writeLine("Hello & welcome to our game (press CTRL/CMD+C to exit)")

  return chain(
    () => writeLine("Done."),
    chain(
      () => gameLoop(dsl),
      intro
    )
  )
}

Play npm start -s:

Tada! We can you try your luck using our hand-made game, almost … you may ask a logical question here - where did I get the game screenshots? And the answer comes right from the future, future chapters, keep reading :).

One thing to mention is that we didn’t only use constructs from the languages we defined, like:

  • if branching: (condition) ? (true handler) : (false handler) - a disjunction type may be used here;
  • Equality checking: guess === num - it works fine in our example, but for more complex data structures - further abstraction is needed (see: Setoit).

Implementation

Finally, we have our application described in abstract terms - our DSLs, to make use of it we will also need an implementation.

At this moment we have to choose a concrete type for our HK<F, _>. In this example I’ll use IO from funfix-exec library.

Console

Define an object implementing our Console interface using concrete IO type:

import { Console } from "../dsl/console"
import { IO } from "funfix-effect"
import * as readline from "readline"
import { FutureMaker, Cancelable } from "funfix-exec"

export const ConsoleIO: Console<"funfix/io"> = {
  writeLine: (line: string) => IO.of(() => {
    console.log(line)
  }),

  readLine: (): IO<string> => IO.deferFuture(() => {
    const cl = Cancelable.empty()
    const ft = FutureMaker.empty<string>()

    const rl = readline.createInterface(process.stdin, process.stdout)

    rl.on("SIGINT", () => {
      rl.close()
      cl.cancel()
    })

    rl.question("> ", (answer) => {
      rl.close()
      ft.trySuccess(answer)
    })

    return ft.future(cl)
  })
}

src: console-io.ts

The readLine function supports cancellation on SIGINT process signal.

Random

import { Random } from "../dsl/random"
import { IO } from "funfix-effect"

export const RandomIO: Random<"funfix/io"> = {
  randomInt: (min: number, max: number): IO<number> => IO.of(() => {
    return Math.floor(Math.random() * (max - min + 1)) + min
  })
}

src: random-io.ts

Numbers

import { Numbers } from "../dsl/numbers"
import { IO } from "funfix-effect"

export const NumbersIO: Numbers<"funfix/io"> = {
  parseInt: (num: string): IO<number> =>
    IO.of(() => parseInt(num, 10))
      .flatMap(n => isFinite(n)
        ? IO.pure(n)
        : IO.raise(new Error(`Failed to parse integer number from "${num}:`))
      )
}

src: numbers-io.ts

Program

import { Program } from "../dsl/program"
import { IO } from "funfix-effect"

export const ProgramIO: Program<"funfix/io"> = {
  pure: <A>(a: A): IO<A> => IO.pure(a),

  chain: <A, B>(f: (a: A) => IO<B>, fa: IO<A>): IO<B> => fa.chain(f),
}

src: program-io.ts

Having all the languages implemented we can use them to compose our app language GameDSL:

export const GameDSL: GameDSL<"funfix/io"> = {
  ...ProgramIO,
  ...ConsoleIO,
  ...NumbersIO,
  ...RandomIO
}

export const gameIO = game(GameDSL)

The only remaining thing is to run the app, and luckily our IO type have the run method!

// The end of the world is here
gameIO.run()

Unfortunately this doesn’t work, as there are no HKTs support in the language so compiler cannot infer the concrete type to know there is a run method, so we will have to add one more small abstraction of this method, same way we did with other DSLs:

export interface App<F> {
  /**
   * Execute given application
   */
  run<A>(fa: HK<F, A>): void
}

src: app.ts

and the implementation:

export const AppIO: App<"funfix/io"> = {
  run: <A>(fa: IO<A>): void => {
    fa.run()
  }
}

src: app.ts

And Play! npm start -s:

AppIO.run(gameIO)

src: app.ts

Testing

A powerful type system already protects us from a lot of troubles which in dynamicaly typed languages, we have to deal with on runtime but in real life applications we rarely never shift app business logic to the type level completely, for various reasons, so there is still place for writing automated tests.

In order to test our game program we will need to write one more implementation which allows to provide test data and observes application side effects.

For this implementation we can use a simpler HK type implementation but it still must be lazy! to guarantee our declarative style program is evaluated in the rigth order!

Let’s call it Eval :)

export type Eval<A> = (() => A) & HK<"Eval", A>

export const Eval = <A>(a: () => A): Eval<A> => a as Eval<A>

And then just draw the rest of the Owl:

const ProgramEval: Program<"Eval"> = {
  pure: <A>(a: A): Eval<A> => Eval(() => a),

  chain: <A, B>(f: (a: A) => Eval<B>, fa: Eval<A>): Eval<B> => Eval(() => f(fa())())
}

const NumbersEval: Numbers<"Eval"> = {
  parseInt: (num: string): Eval<number> => Eval(() => parseInt(num, 10))
}

/**
 * This is a container for app state
 *  `readLines` property used to pass user inputs
 *  `writeLines` property used to collect messages wrote by the app
 *  `randoms` "random" numbers device
 */
class TestState {
  constructor(public readLines: string[],
              public writeLines: string[],
              public randoms: number[]) {}

  /**
   * Return input lines from a predefined list
   */
  readLine(): string {
    const head = this.readLines[0]
    this.readLines = this.readLines.slice(1)

    return head
  }

  /**
   * Collect all lines wrote
   */
  writeLine(line: string): void {
    this.writeLines.push(line)
  }

  /**
   * Issue "random" numbers
   */
  randomInt(min: number, max: number): number {
    const head = this.randoms[0]
    this.randoms = this.randoms.slice(1)

    return head
  }
}

function TestModule(state: TestState): GameDSL<"Eval"> {
  return {
    ...ProgramEval,
    writeLine: (line: string): Eval<void> => {
      return Eval(() => state.writeLine(line))
    },
    readLine(): Eval<string> {
      return Eval(() => state.readLine())
    },
    randomInt(min: number, max: number): Eval<number> {
      return Eval(() => state.randomInt(min, max))
    },
    ...NumbersEval
  }
}

const AppEval: App<"Eval"> = {
  run: <A>(fa: Eval<A>): void => {
    fa()
  }
}

TestState class represents our mutable world, providing us resources we need and accepting our output back.

Here is how test case will look like:

describe("game", () => {
  it("succeeds for right guess", () => {
    const state = new TestState(["1", "n"], [], [1])
    const testModule = TestModule(state)

    // Run the app and mutate the state
    AppEval.run(game(testModule))

    expect(state.readLines, "all lines read").is.empty
    expect(state.writeLines, "wrote the right output").deep.equals([
      "Hello & welcome to our game (press CTRL/CMD+C to exit)",
      "Please enter a number from 1 to 5",
      "You Win!",
      "Do you want to play again? [Y/n]:",
      "Done."
    ])
    expect(state.randoms, "all randoms consumed").is.empty
  })
})

src: game-app.test.ts

App will first receive the random number 1 we provided in TestState, then user will enter his guess "1", and will answer with "n" to not continue the game. All the app output will be also collected into state.writeLines and compared to expected messages list.

Note on Variance

Functions in TypeScript are contravariant on their arguments (with --strictFunctionTypes option On). For interface methods - bivariant first assignement is still permited and we will have to use bivariant definition when we have HKTs in contravariant positions (function arguments) so compiler can match lighweight HKTs to concrete type we used, ex:

import { HK } from "funland"
import { Eval } from "funfix-effect"

export interface Program<F> {
  // declared as a method
  pass<A>(fa: HK<F, A>): HK<F, A>
}

export const ProgramEval: Program<"funfix/eval"> = {
  pass: <A>(fa: Eval<A>): Eval<A> => fa
}

vs when declard as as a property:

import { HK } from "funland"
import { Eval } from "funfix-effect"

export interface Program<F> {
  // declared as a property
  pass: <A>(fa: HK<F, A>) => HK<F, A>
}

export const ProgramEval: Program<"funfix/eval"> = {
  pass: <A>(fa: Eval<A>): Eval<A> => fa
}

Issues an error:

test/bivariance.test.ts:36:3 - error TS2322: Type '<A>(fa: Eval<A>) => Eval<A>' is not assignable to type '<A>(fa: HK<"funfix/eval", A>) => HK<"funfix/eval", A>'.
  Types of parameters 'fa' and 'fa' are incompatible.
    Type 'HK<"funfix/eval", A>' is missing the following properties from type 'Eval<A>': get, map, flatMap, chain, and 5 more.

36   pass: <A>(fa: Eval<A>): Eval<A> => fa
     ~~~~

So, when you use HKTs as arguments to your DSL functions - declare them as interface methods, not as properties.

Conclusions

Overall the results looks pretty intriguing. We obtained an implementation agnostic, declarative style application, easy to compose and extend.

And I didn’t even use the scary M… word once so far! Did I? But the Monad, if you’re somehow familiar with it, probably came into your mind while looking at our Program<F> language as you may spot there some generic functionality which you will use with all of your programs.

However, there are still questions needs to be answered:

  • How can I introduce this approach gradually into my project?
  • How do I share my generic code?
    • Should I provide a test kit for language laws?
    • Should I provide a default implementation? if so - pack it into same package?
  • Does it scale?
    • The final app should speak all the languages used inside, and composing it as an intersection type may lead to conflicts - how do I isolaenamespace my languages?
    • How to set type bounds on domain data and/or languages? and how to bring those type classes in?
  • Do I really need to define all the languages myself?
    • Here I invite you to join me diging deeper into comonly used type classes, starting with a really nice hierarchy visual representation: cats typeclasses;
  • How to handle errors or combine different arity kinds?

On all of these I’ll elaborate within my next posts - Follow Me and stay tuned!

PS: Lear more languages! Even if you won’t use them in production, it will definitely help you take a fresh look and make you better in your primary programming language.