JavaScript
Node.js

JavaScript で Maybe(Option/Optional/Nullable)

JavaScriptで関数型プログラミングをしてみようかな、と思う今日この頃。ざっくりいって Ramda と folktale の二つを使いこなせばいいんじゃない?って StackOverflow が教えてくれた 。あとドキュメントとして fantasy-land。

とりあえず、null の扱いがあんまり好きじゃないので、 Maybe から始めてみる。Scala でいうところの Option。まずはMaybeを作るところから。Just, Nothing, fromNullable() の三つの方法を試してみた。Just(null)Nothing にならないところが注意。nullを渡してNothing にしたい場合は fromNullable() を使おう。

const { Just, Nothing, fromNullable } = require('folktale/maybe');
const assert = require('assert');

// Create Maybe
{
  const justs = [
    Just(1),
    Just({ hello: 'world' }),
    Just(null), // you can pass any falthy. returns Just whose value is falthy
    Just(undefined),
    Just([]),
    Just(''),
  ];
  justs.forEach((m) => {
    assert(m instanceof Just);
  });
  const nothings = [
    Nothing(),
    Nothing(null), // any argments will be ignored.
  ];
  nothings.forEach((m) => {
    assert(m instanceof Nothing);
  });
  const js = [
    fromNullable(1),
    fromNullable({ one: 1 }),
    fromNullable(''),
    fromNullable([]),
  ];
  js.forEach((m) => {
    assert(m instanceof Just);
  });
  const ns = [
    fromNullable(null),
    fromNullable(undefined),
  ];
  ns.forEach((m) => {
    assert(m instanceof Nothing);
  });
}

Maybeを作ったら、Maybe を変換する関数を作って、mapchainをつなげてどんどん変換していく。mapは引数に値を返す関数をとるのに対して、chainでは引数に明示的にMaybeを返す関数をとる。自分で実装するときはchainを使うのがいいのかなあ?と思ったりした。Maybeから値を取り出すには getOrElse を使う。Nothingに対して処理をしたい場合はorElseを使う。

const { Just, Nothing, fromNullable } = require('folktale/maybe');
const assert = require('assert');
// Transform Maybe
{
  const funcForChain = val => Just(val + 2);
  assert.equal(3, Just(1).chain(funcForChain).getOrElse(null));
  assert.equal(2, Just(null).chain(funcForChain).getOrElse(null)); // NOTE: null + 2 = 2
  assert.equal(null, Nothing().chain(funcForChain).getOrElse(null));

  const funcForMap = val => val + 2;
  assert.equal(3, Just(1).map(funcForMap).getOrElse(null));
  assert.equal(2, Just(null).map(funcForMap).getOrElse(null));
  assert.equal(null, Nothing().map(funcForMap).getOrElse(null));

  const getName = obj => ('name' in obj ? Just(obj.name) : Nothing());
  const getDefaultName = () => Just('anon');
  assert.equal(
    'Alice',
    Just({ name: 'Alice' }).chain(getName).orElse(getDefaultName).getOrElse(null),
  );
  assert.equal(
    'anon',
    Just({}).chain(getName).orElse(getDefaultName).getOrElse(null),
  );
  const toUpper = str => Just(str.toUpperCase());
  assert.equal(
    'ALICE',
    Just({ name: 'Alice' }).chain(getName).orElse(getDefaultName)
      .chain(toUpper)
      .getOrElse(null),
  );
  assert.equal(
    'ANON',
    Just({}).chain(getName).orElse(getDefaultName)
      .chain(toUpper)
      .getOrElse(null),
  );
  assert.equal(
    'anon',
    Just({}).chain(getName).chain(toUpper)
      .orElse(getDefaultName)
      .getOrElse(null),
  );
}

「値を Maybe に詰め込んで、 Maybe の世界で map と chain で次々と処理していく」という処理パターンは、「値を Promise に詰め込んで、Promise の世界で then で次々と処理していく」という処理パターンと類似しているという ブログ記事 があって、なるほどなあ、と思った。