6
\$\begingroup\$

Here's my implementation of State Monad in TypeScript, based on a canonical Haskell implementation. I would like it to get code reviewed.

class StateMonad<S, A> {
  constructor(public runState: (s: S) => ({ s: S, a: A })) {
  }

  static return_<S, A>(a: A): StateMonad<S, A> {
    return new StateMonad(s => ({ s, a }));
  }

  bind<B>(func: (a: A) => StateMonad<S, B>): StateMonad<S, B> {
    return new StateMonad<S, B>((s: S) => {
      const { s: s_, a } = this.runState(s);
      return func(a).runState(s_);
    });
  }
}

// aux monad factory
const createCounter = (regex: RegExp) => new StateMonad((s: string) =>
  s.split('')
    .reduce((acc, c) =>
      (regex.test(c)) ? { s: acc.s.replace(c, ''), a: acc.a + 1 } : acc,
      { s, a: 0 })
);

const countLowerCase = createCounter(/[a-z]/);
const countDigits = createCounter(/[0-9]/);


// usage example
const { a } = countLowerCase   /* -- haskell equivalent   */
  .bind(n1 => countDigits      /* do n1 <- countLowerCase */
    .bind(n2 => StateMonad     /*    n2 <- countDigits    */
      .return_(n1 + n2)))      /*    return n1 + n2       */
  .runState("abc123ABC");
\$\endgroup\$
  • \$\begingroup\$ I am not big on functional styles but as I understand it the state object should be a new copy rather than just the reference moved to a new object? Or is the responsibility of isolating the state in the function using StateMonad? If so why would you need it? \$\endgroup\$ – Blindman67 Apr 7 '18 at 12:57
  • \$\begingroup\$ @Blindman67 I added a usage example, of how its intended to be used. Not sure what you mean new copy/reference moved. State gets "mutated" as it gets passed through a chain of monads. \$\endgroup\$ – dark_ruby Apr 7 '18 at 16:31
  • \$\begingroup\$ I wonder how does this line work: const { s: s_, a } = this.runState(s);. Object decomposition seems to be broken in part s: _s. Where does the _s come from? Is it a bug or I am missing something? \$\endgroup\$ – Igor Soloydenko Apr 11 '18 at 19:23
  • \$\begingroup\$ @IgorSoloydenko no, it's not a bug, it's a feature :) you can rename decomposed variable to a different name, I'm doing it because s is already in scope. In fact you could have copied this code typescript online compiler and seen what it compiles to. \$\endgroup\$ – dark_ruby Apr 11 '18 at 19:53
  • 1
    \$\begingroup\$ I tried writing a review, but really don't see anything to complain about besides return_ being ugly for a public interface (just use return) and createCounter being unnecessarily complex (just make the regular expression global and a will be the difference in lengths of the resulting string and original string) \$\endgroup\$ – Gerrit0 Apr 12 '18 at 22:38
1
\$\begingroup\$

I'm probably off my rocker here, but what if rather than nested chains of bind, you used a fork/join type of approach?

static combine<T, S, R>(
    monads: { [P in keyof T]: StateMonad<S, T[P]> },
    selector: (values: T) => R
)
    : StateMonad<S, R> {
    return new StateMonad<S, R>((state) => {
    var ret: any = {};
    for (const key in monads) {
        if (monads.hasOwnProperty(key)) {
        ret[key] = monads[key].runState(state).a;
        }
    }
    return { a: selector(ret), s: state }
    }
    );
}

and

var { a } = StateMonad
  .combine(
    {
      countLowerCase,
      countDigits
    },
    combined =>
      combined.countLowerCase +
      combined.countDigits
  )
  .runState("abc123ABC");
\$\endgroup\$
  • 1
    \$\begingroup\$ Welcome to Code Review! You have presented an alternative solution, but haven't reviewed the code. Please explain your reasoning (how your solution works and why it is better than the original) so that the author and other readers can learn from your thought process. \$\endgroup\$ – Dannnno Jun 21 '18 at 2:26

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.