5
\$\begingroup\$

Having been introduced to monads through Scala, I am trying to add the generalized design pattern to my toolkit by implementing it in a language I use more regularly (typescript).

My tests pass (bottom snippet), but I worry that I am committing heresies and/or doing it wrong. Could anyone suggest improvements?

I'll post code here, but the source is accessible and easily runnable/testable on branch stackExchangeCodeReview of this repo (requires node).

I have a base class, Monad<T>...

export type mapping<T, U> = (x: T, ...args: any[]) => U;

/**
 * Describes the base monadic operations and holds state in two properties:
 *
 *   `_wrapped` is the monadic container, managed in sub-classes
 *
 *   `_unwrapped` is the original value passed to the constructor (unit).  This is included for convenience, since
 *       the `_wrapped` value is not guaranteed to preserve the monad's input, and it could be difficult to
 *       functionally map back to that input.
 */

export abstract class Monad<T> {

  public abstract wrapped: any;
  public abstract unit: <T>(x: T, ...args: any[]) => Monad<T>;
  public abstract flatMap: <U>(f: mapping<T, Monad<U>>) => Monad<U>;

  private _unwrapped: T;
  public get unwrapped(): T {
    return this._unwrapped;
  }

  constructor (x: T, ...args: any[]) {
    this._unwrapped = x;
  }

}

... which is used to create a sub-class Logger<T>, which performs operations and keeps a log of those operations. (inspired by this JS implementation of Haskell's writer monad)

import {Monad, mapping} from './monad';

/**
 * Keeps a log of operations and their return values by accepting labels for lifted operations.
 */

interface ILog <T> {
  value: T;
  log: string;
}

export class Logger<T> extends Monad<T> {

  private _wrapped: ILog<T>;
  public get wrapped(): ILog<T> {
    return this._wrapped;
  }

  public static lift = <T, U>(f: mapping<T, U>, label: string) => (x: T, log: string) => new Logger(f(x), log + '\n' + label);

  constructor (x: T, log: string = 'unit') {
    super(x);
    this._wrapped = { value: x, log: `${log} -> ${x}` };
  }

  public unit = (x: T, log: string) => new Logger(x, log);

  public flatMap = <U>(f: mapping<T, Logger<U>>) => f(this.unwrapped, this.wrapped.log);

}

It can be used like this...

import {Logger} from './monads/logger';

const double = (n: number) => n * 2;
const square = (n: number) => n * n;
const gtTen = (n: number) => n > 10;

const lDouble = Logger.lift(double, 'double');
const lSquare = Logger.lift(square, 'square');
const lGtTen = Logger.lift(gtTen, 'greater than ten');

const chain = new Logger(2).flatMap(lDouble).flatMap(lSquare).flatMap(lGtTen);

console.log(chain.wrapped.log, '\n\nwrapped.value: ', chain.wrapped.value, '\n\nunwrapped: ', chain.unwrapped);
// unit -> 2
// double -> 4
// square -> 16
// greater than ten -> true 
// 
// wrapped.value:  true 
// 
// unwrapped:  true

... and I am testing that it fulfills the monadic contract in these passing tests:

import {mapping} from './monad';
import {Logger} from './logger';

describe('logger', () => {

  it('should initialize', () => {
    expect(Logger).toBeDefined();
  });

  it('should fulfill left unit law: `unit(x).flatMap(f) == f(x)`', () => {
    const x = 1;
    const f: mapping<number, Logger<number>> = (n: number) => new Logger(n);
    const leftSide = new Logger(x).flatMap(f);
    const rightSide = f(x);

    const expected = jasmine.objectContaining({
      unwrapped: rightSide.unwrapped,
      wrapped: rightSide.wrapped
    });

    expect(leftSide).toEqual(expected);
  });

  it('should fulfill right unit law: `unit(x).flatMap(unit) == unit(x)`', () => {
    const x = 1;
    const leftSide = new Logger(x);
    const rightSide = leftSide.flatMap(leftSide.unit);

    const expected = jasmine.objectContaining({
      unwrapped: rightSide.unwrapped,
      // Logs in `wrapped` will not match since left and right side go through different operations
      // wrapped: rightSide.wrapped
    });

    expect(leftSide).toEqual(expected);
  });

  it('should fulfill law of associativity: `unit(x).flatMap(f).flatMap(g) == unit(x).flatMap(x => f(x).flatMap(g))`', () => {
    const x = 1;
    const f: mapping<number, Logger<number>> = (n: number) => new Logger(n, 'f');
    const g: mapping<number, Logger<number>> = (n: number) => new Logger(n + 2, 'g');
    const m = new Logger(x);
    const leftSide = m.flatMap(f).flatMap(g);
    const rightSide = m.flatMap(num => f(num).flatMap(g));

    const expected = jasmine.objectContaining({
      unwrapped: rightSide.unwrapped,
      wrapped: rightSide.wrapped
    });

    expect(leftSide).toEqual(expected);
  });

  it('should keep a log of all operations and return values', () => {
    const double = (n: number) => n * 2;
    const square = (n: number) => n * n;
    const gtTen = (n: number) => n > 10;

    const lDouble = Logger.lift(double, 'double');
    const lSquare = Logger.lift(square, 'square');
    const lGtTen = Logger.lift(gtTen, 'greater than ten');

    const chain = new Logger(2).flatMap(lDouble).flatMap(lSquare).flatMap(lGtTen);
    expect(chain.wrapped.value).toBe(true);
    expect(chain.wrapped.log).toBe('unit -> 2\ndouble -> 4\nsquare -> 16\ngreater than ten -> true');
  });

});
\$\endgroup\$
3
\$\begingroup\$
export type mapping<T, U> = (x: T, ...args: any[]) => U;

export abstract class Monad<T> {
  public abstract unit: (x: T, ...args: any[]) => Monad<T>;
  public abstract flatMap: <U>(f: mapping<T, Monad<U>>) => Monad<U>;
}

I've dropped everything that did not match the monad typeclass members in Haskell.

  • There is no need to store wrapped since this is already represented by this.
  • I've removed unwrapped. It conveys the idea that a monad always wraps a value from an underlying type. However, this artificially restricts the idea.

export class Logger<T> extends Monad<T> {

  private _log: string[];
  public get log(): string[] { return this._log; }
  private _value: T;
  public get value(): T { return this._value; }

  public static liftLogger = <T2, U>(f: mapping<T2, U>, label: string) => ((x: T2) => new Logger(f(x), [label]));

  constructor (x: T, log: string[] = []) {
    super();
    this._value= x;
    this._log= log;
  }

  public unit = (x: T, log: string): Logger<T> => new Logger(x, [log]);

  public flatMap = <U>(f: mapping<T, Logger<U>>) => {
    let x = f(this.unwrapped);
    return new Logger(x.unwrapped, [...this.wrapped, ...x.wrapped]);
  }
}
  • I've removed the ILog<T> interface. It stored another value that was already stored.
  • To match implementations with the Writer monad, I've changed the log entry type from string to string[].
  • lift did not only lifted the function, it implemented the monadic bind for the Logger<T> type. This is now done inside flatMap.

  • I've not looked at the MList type.
  • I'd like to see an implementation that did not rely on Monad<T> being a base class. In Haskell, a type is made a monad by creating a Monad typeclass instance for it.

Edit 1: Rename _wrapped -> _log, _unwrapped -> value

\$\endgroup\$
  • \$\begingroup\$ Thanks, this is great! I've skimmed and will have a closer look Sat. \$\endgroup\$ – Manningham Jul 11 '17 at 7:11
  • \$\begingroup\$ These are some nice improvements and I've implemented and simplified the syntax a bit on this branch. I made Monad an interface instead of a base class, which I suppose is as close to a Haskell typeclass as we can get in Typescript. I re-implemented some logic to add the values to the operations log, since it seemed like a shame to have lost that in your revision. Thanks for your feedback! \$\endgroup\$ – Manningham Jul 17 '17 at 4:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.