LoginSignup
20

More than 3 years have passed since last update.

TypeScriptで関数型っぽくプログラミングする

Last updated at Posted at 2019-12-06

@maxfie1dと申します。Webが得意なフロントエンドエンジニアです。
Qiitaの記事は久しぶりなので少し緊張気味です。

この記事はTypeScript(TS)で関数型っぽくプログラミングしてみようという記事です。なぜ関数型っぽく書くかというと、明確で分かりやすく簡単なコードになるからです。いやマジで!

この記事で言う関数型っぽいとはどういうことかというと...

  • Maybe<T>, Result<E, T>
  • カリー化
  • イミュータブル
  • パイプ、合成関数
  • パターンマッチ

これらを実現できたら(僕的には)充分です。順番にやってみましょう!

※詳しい用語や背景の説明は飛ばすので、関数型言語を触ったことの無い人にはつらいかもしれません。ですが、サンプルコードを見れば大体の動きは想像できると思います。とにかく、こういう書き方できるんやなーと知ってくれたら嬉しいです!ライブラリにFolktaleRamdaを使用します。

Maybe<T>, Result<E, T>

TSだと一般的にundefinednull、もしくは例外で「値が存在しないこと」や「失敗」を表しますが、Maybe<T>Result<E, T>を使うという方法もあります。

Maybeの例
import { maybe, Maybe } from "folktale";

const myMaybe: Maybe<number> = trueOrFalse()
  ? maybe.Just(123)
  : maybe.Nothing();

myMaybe.matchWith({
  Just: ({ value }) => {
    console.log(value); // => 123
  },
  Nothing: () => {
    console.log("値がないよ");
  }
});
Resultの例
import { result, Result } from "folktale";

const myResult: Result<string, number> = trueOrFalse()
  ? result.Ok(123)
  : result.Error("失敗だよ");

myResult.matchWith({
  Ok: ({ value }) => {
    console.log(value); // => 123
  },
  Error: ({ value }) => {
    console.log(value); // => "失敗だよ"
  }
});

Maybe<T>Result<E, T>の違いは、異常の場合に値を持つかどうかです。

イミュータブル

TSだと、配列の要素追加をarray.push(...)で行いますが、これは要素を追加した新しい配列を返すのではなく既存の配列に値が追加されます。つまりミュータブルです。(ちなみにArray.pushの返り値の型はnumberで新しい配列の長さが返されます。)

イミュータブルにやってみましょう。

イミュータブルな配列操作の例

import * as R from "ramda";

const array = [1, 2, 3];
// ミュータブル
array.push(4);

console.log(array);  // => [1, 2, 3, 4]

// イミュータブル
const array5 = R.append(5, array);
const array0 = R.prepend(0, array);

console.log(array);  // => [1, 2, 3, 4]
console.log(array5); // => [1, 2, 3, 4, 5]
console.log(array0); // => [0, 1, 2, 3, 4]

R.appendR.prependも元の配列arrayに影響を与えません。つまりイミュータブルです。やったね!

カリー化

朗報!
カリー化されていない関数を後からカリー化することができます。

カリー化の例
import * as R from "ramda";

const add3 = (a: number, b: number, c: number) => a + b + c;
const curriedAdd3 = R.curry(add3);

console.log(add3(1, 2, 3));        // => 6

console.log(curriedAdd3(1)(2, 3)); // => 6
console.log(curriedAdd3(1, 2)(3)); // => 6
console.log(curriedAdd3(1)(2)(3)); // => 6

パイプ、合成関数

僕はF#で関数型言語を学んだのですが、F#にはPipeline Operator |>というものがあります。パイプライン演算子を使うと、通常*関数(引数)の順で記述するところを逆の引数 |> 関数*の順で記述することができます。

他にもF#にはComposition Operator >>というものがあり、f >> gのようにして合成関数を作ることができます。

残念ながらTSでは|>>>といった演算子は使用できませんが、似たことをRamdaを使ってやってみましょう。pipecomposeの違いは関数の適用順序です。

pipeとcomposeの例
import * as R from "ramda";

type Fn = (xs: number[]) => number[];
const f: Fn = R.filter(x => x % 2 === 0);
const g: Fn = R.map(x => x * 2);
const h: Fn = R.filter(x => x > 5);
const numbers = R.range(1, 10);

const r1 = R.pipe(f, g, h)(numbers);
const r2 = R.compose(f, g, h)(numbers);

console.log(r1); // => [8, 12, 16]
console.log(r2); // => [12, 14, 16, 18]

関数をパイプしたり合成するメリットの1つは不要な一時変数(しばしば命名にも悩む)を置く必要がなくなることです。

ちなみに、2019年11月26日現在パイプライン演算子はECMAScriptでstage-1なんですね。知らなった。try-babelで試せます(presetsstage-1をオンにしておきましょう)。

将来的にTSでもパイプライン演算子が使えるようになったら嬉しいですね。

(ちなみにちなみに、記事を書いている途中にRubyの開発版でパイプライン演算子が導入されたことを知りました。まじか。)

パターンマッチ

関数型っぽく書きたいならパターンマッチも欲しいですよね。しかし、残念ながら現状では難しいみたいです。提案はstage-1で、将来的には使えるようになるかもしれませんがいつのことになるやら。

どうしてもやってみたい人は[babelの実装](Pattern-matching with babel https://github.com/babel/babel/pull/9318)や[funcy](https://github.com/bramstein/funcy)などを使ってみましょう。

おしまいに

FolktaleRamdaを使うことで、TSでも結構関数型っぽく書くことができて関数型スタイルの恩恵を受けることができます。

他にもJSのプラットフォームで関数型を使うアイデアとして、例えばF#をJSにコンパイルするFable、JSコンパイルに対応しているKotlin、AltJSのLiveScriptPureScriptReasonを使うという手があります。

awesome-fp-jsを覗いてみるともっと面白い発見があるかもしれません!

リソース

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20