@maxfie1dと申します。Webが得意なフロントエンドエンジニアです。
Qiitaの記事は久しぶりなので少し緊張気味です。
この記事はTypeScript(TS)で関数型っぽくプログラミングしてみようという記事です。なぜ関数型っぽく書くかというと、明確で分かりやすく簡単なコードになるからです。いやマジで!
この記事で言う関数型っぽいとはどういうことかというと...
-
Maybe<T>
,Result<E, T>
- カリー化
- イミュータブル
- パイプ、合成関数
- パターンマッチ
これらを実現できたら(僕的には)充分です。順番にやってみましょう!
※詳しい用語や背景の説明は飛ばすので、関数型言語を触ったことの無い人にはつらいかもしれません。ですが、サンプルコードを見れば大体の動きは想像できると思います。とにかく、こういう書き方できるんやなーと知ってくれたら嬉しいです!ライブラリにFolktaleとRamdaを使用します。
Maybe<T>, Result<E, T>
TSだと一般的にundefined
やnull
、もしくは例外で「値が存在しないこと」や「失敗」を表しますが、Maybe<T>
やResult<E, T>
を使うという方法もあります。
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("値がないよ");
}
});
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.append
もR.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を使ってやってみましょう。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で試せます(presets
のstage-1
をオンにしておきましょう)。
将来的にTSでもパイプライン演算子が使えるようになったら嬉しいですね。
(ちなみにちなみに、記事を書いている途中にRubyの開発版でパイプライン演算子が導入されたことを知りました。まじか。)
パターンマッチ
関数型っぽく書きたいならパターンマッチも欲しいですよね。しかし、残念ながら現状では難しいみたいです。提案はstage-1で、将来的には使えるようになるかもしれませんがいつのことになるやら。
どうしてもやってみたい人は[babelの実装](Pattern-matching with babel https://github.com/babel/babel/pull/9318)や[funcy](https://github.com/bramstein/funcy)などを使ってみましょう。
おしまいに
FolktaleとRamdaを使うことで、TSでも結構関数型っぽく書くことができて関数型スタイルの恩恵を受けることができます。
他にもJSのプラットフォームで関数型を使うアイデアとして、例えばF#をJSにコンパイルするFable、JSコンパイルに対応しているKotlin、AltJSのLiveScriptやPureScriptやReasonを使うという手があります。
awesome-fp-jsを覗いてみるともっと面白い発見があるかもしれません!