LoginSignup
148
132

More than 1 year has passed since last update.

初めてでもこわくない!TypeScriptで関数型プログラミングをしてみよう

Last updated at Posted at 2022-10-16
1 / 248

この記事は「技育祭」というイベントで発表したものです


是非「スライドモード」でご覧ください


みなさん、凶悪な関数はお好きですか?


最近弊社では「ゆめみからの挑戦状」というクイズ企画をTwitter上で行なっています


その企画の中で、こんな問題を出したことがあります


「足し算関数に1行追加して、凶悪にしてください」


元となる足し算関数

TypeScript
  const add = (a: number, b: number): number => {
    // ここに1行追加して、凶悪な関数にしてください。
    return a + b
  }

すると、たくさんのエンジニアさん達が
さまざまな凶悪な処理を考えてくれました


いくつか見ていきましょう


TypeScript
  const add = (a: number, b: number): number => {
+   if (Date.now() >= Date.parse('2023/01/01')) return 0;
    return a + b
  }

2023年になると、0を返すようになる関数です


これは凶悪ですね・・・!


2022年の間はちゃんと足し算をしてくれるので


自動テストなんかもすり抜けてしまいます・・・!


もう一つ見てみましょう


TypeScript
  const add = (a: number, b: number): number => {
+   process.env.ENV = "DEV";
    return a + b
  }

足し算をすると、
環境変数を上書きしてしまう関数です


本番環境なのに、足し算をしたら
開発環境モードの動作に変わってしまうんでしょうか


恐ろしいですね・・・!


エンジニアの皆さんは、
凶悪なことを考えるのが本当にお得意でいらっしゃいます・・・!


このような凶悪なコード、
大喜利の解答として見ている分には面白いんですが


「実際にプロダクトのコードに紛れ込んでしまったら」
と考えると、恐ろしくてたまりません


間違って凶悪な処理を書かないように、
気をつけないといけませんね


でも、気をつけなくても──


これらの凶悪な処理をそもそも書けない──


そんなプログラミング言語もあります


その一つが、純粋関数型言語Elmです


Elmでは「参照透明」な関数しか書けません


参照透明とはなんでしょうか?


まずは先ほどのTypeScriptの足し算関数を見てみましょう


TypeScript
  const add = (a: number, b: number): number => {
    if (Date.now() >= Date.parse('2023/01/01')) return 0;
    return a + b
  }

この関数は、例えば35を渡したら8を返してくれます


でも、2023年になると正しい答えを返さなくなります


どんな引数を渡しても0を返すようになってしまいます


現在時刻によって、返す値が変わってしまう関数です


抽象的には「外部の状態に依存する関数」です


35を入れたら、必ず8を返してほしいですよね?


2023年になっても、いつまでも8を返してほしいものです


35を入れたら、必ず8を返してくれる


同じ引数を渡したら、必ず同じ結果を返してくれる


その性質を「参照透過性・参照透明性」と言います。


でも、先ほどの凶悪な足し算関数は
日時に依存しているため、参照透明ではありません


参照透明だと、何が嬉しいのでしょうか?


結果が予測しやすい


参照透明な関数は、
同じ引数を渡せば、いつも同じ答えを返します


場面によって返す答えが変わったりしません


なので、挙動が予測しやすく、
自動テストとの相性も良いです


  • 3と5を渡したら、8が返ってきたね!
  • 1と10を渡したら、11が返ってきたね!

↑こんな風に、自動テストで動作を保証できます


参照透明でない関数は、テストしづらかったりします


先ほどの、日時に依存している関数とかですね


同じ引数を渡しても、
場面によって返ってくる値が変わるためです


ただ、テストできなくもないです


JavaScriptのDateをモック化してくれる
ライブラリなどもあります


もしくは──


2022年 大晦日

ワイ「ついにこの日が来たで・・・!」
ワイ「今、23時30分や・・・」
ワイ「そろそろ年越しテストを開始するで!」


ワイ「npm run test!」
ワイ「npm run test!」
ワイ「npm run test!」
ワイ「npm run test!」


2023年、到来

ワイ「お、年越した?年越した?」
ワイ「年越したな!」
ワイ「npm run test!」
ワイ「お、戻り値が変わった!」
ワイ「よっしゃ!テスト成功や!」


「年越しのタイミングで、ちゃんと関数の戻り値が変わったで!」


きっと、一生の思い出に残るテストになると思います


話を戻します


参照透明な関数には、ほかにもメリットがあります


どんなに重い計算でも、引数が分かればメモ化できます


外部の状態を気にしなくてよいので、再利用性も高いです


2つめの凶悪な関数についてはどうでしょうか


TypeScript
  const add = (a: number, b: number): number => {
+   process.env.ENV = "DEV";
    return a + b
  }

計算の結果を返せばいいだけなのに、
外部の状態を変えてしまっています


関数が外部の状態を変えたり、
外部に影響を与えたりすることを「副作用」と呼びます


逆に、副作用がなく参照透明な関数は
「純粋関数」と呼ばれます


純粋関数型言語であるElmでは、皆さんの書くコードが
純粋関数となるように設計されています


同じ引数を渡せば、必ず同じ答えが返ってきます


じゃあ、場面によって答えを変えたい場合はどうすればいいの?


例えば「今日は○月○日です!」という
文字列を生成してくれる──


そんな関数を作りたい場合はどうすればいいの?


そういう関数は、日によって返す値が違うはずじゃん!


純粋関数しか書けなかったら、
そういう関数が作れないじゃん!


そこは、ちょっと不思議な仕組みで可能にします


Elmという言語では、

  1. 現在の日時を取得する
  2. その日時を元に「今日は○月○日です!」という文字列を生成して、返す

↑これを、1つの関数の中で行うことができません


ではどうするのかというと、

  1. 「現在の日時を取得してくれい!」というお手紙をElmランタイムに渡す
  2. Elmランタイムから、現在の日時が渡ってくる
  3. 受け取った日時を元に「今日は○月○日です!」という文字列を生成して、返す

↑このように、どこかで「Elmランタイムにお願いする」フェーズを挟みます


そのため、1つの関数の中で

  1. 現在の日時を取得!
  2. その時刻を元に文字列を生成して、返す!

↑これができません


「現在の日時を取得して、メッセージ文を生成して、返す」
という関数が書けないのです


でも、Elmランタイムにお願いして
現在の日時を渡してもらうことはできるので


結果的に「今日は○月○日です!」という
文字列を生成することはできます。


日時に依存するような処理も
純粋関数だけを組み合わせて実現できる──


ちょっと不思議な仕組みです


TypeScriptの場合は、以下のようにすると良いと思います。

TypeScript
// 「日時に応じたメッセージを作ってくれる関数」 を実行
const dateMessage = createDateMessage(Date.now());

関数の中で日時を取得するのではなく、
引数として日時を渡す感じです。


こうすることで関数が参照透明になり、
自動テストがしやすくなります


引数として渡す日時を色々変えたりして
テストをすることができますね


TypeScriptでも、意識すれば関数型っぽく書けそうです


では、乱数を扱う場合はどうでしょうか?


純粋関数だけで、できるのでしょうか?


Elmでは、乱数なども似たような方法で扱います


  1. 乱数を生成!
  2. その数値を使って計算し、結果を返す!

↑これを1つの関数の中で行うことができません


どうするかというと、

  1. 「乱数を作ってくれい!」というお手紙をElmランタイムに渡す
  2. Elmランタイムから、生成した数値が渡ってくる
  3. 受け取った数値を元に計算し、値を返す

↑やはり、どこかで「Elmランタイムにお願いする」フェーズを挟みます


そのため「実行するたびに違う数値を返す関数」は
書けないのですが──


ランタイムにお願いして乱数を作ってもらって、
それを受け取って画面に表示する


ということはできます


サイコロを表示するWebページなんかも作れます


参照透明な関数だけを組み合わせて、
乱数を扱えます


Elmでは、他にも凶悪な処理が書きづらくなっています


Elmでは、

TypeScript
  const add = (a: number, b: number): number => {
+   process.env.ENV = "DEV";
    return a + b
  }

↑こういった、勝手に上書きしちゃう関数も書けません


全ての値がイミュータブル(不変)です


再代入はできません


再代入という概念がありません


なので、上書きもできません


「足し算を実行したら、知らん間に環境変数が変わっとった!」


といった副作用も起こりません


このように、純粋関数型言語では──

  • 参照透明でない関数
  • 副作用を起こす関数

を書けないため、
冒頭で紹介したような凶悪な関数が書けません


これは、言語仕様上そうなっているからです。


「全てを予測可能にしたい!」という思想のもとで
言語が作られているからです


詳しくは「Elm Guide」でググってみてください


とても分かりやすい、日本語版の公式ドキュメントがあります


でも──


TypeScriptでは「再代入」もできますし


「1つの関数の中で、乱数を生成しつつ計算」も
できちゃいますよね?


なので、凶悪な副作用も普通に起こせます


参照透明ではない、
ころころ結果が変わる関数も書けちゃいます


じゃあ「TypeScriptで関数型プログラミング」とか
考えてもしょうがなくない?


いえ、むしろ逆です


TypeScriptでこそ、
関数型プログラミングを意識するメリットがあります


いかに純粋関数とそうでない関数を分離できるか、を
意識することが大事です


さっきのコードのような
「そもそも必要のない処理」を取り除くことも大事です


ところで皆さん


undefinedNaNはお好きですか?


TypeScriptでは、
tsconfigでstrict: trueに設定していても


気づかないうちに
undefinedNaNが入り込んでしまう場合があります


(2022年10月現在)


すると、予期せぬエラーが発生してしまうことがあります


User型の値を扱っているつもりが」
「いつの間にかundefinedになってて」
「ブラウザでエラーが出てもうた!」


そんなことが起きてしまうことがあります


しかも、いちいち気をつけてundefinedNaN
チェックするのも大変ですよね


今日からは Option<T> という型を使うことにしましょう


この型を実装するのは面倒なので……


今回は fp-ts というライブラリを使います


「ユーザーが入力した文字列を、数値に変換する」
という例を考えてみます


TypeScriptだと

TypeScript
const num: number = Number("5")

または

TypeScript
const num: number = parseInt("5")

こうですね


でも──


TypeScript
const num: number = Number("あああ")

こうすると、変数numの値はNaNになってしまいます


しかも、NaNnumber型の一種という扱いなので


コンパイルエラーで気づけません


普通にnumber型の変数に代入できてしまいます


明らかに例外的な値なのに、です


(2022年10月現在)


そのため──


画面にNaNって表示してしまったりするミス、
たまに起こります


私もこの間やらかしました


せっかくTypeScriptを使っているので


ブラウザ上でチェックして、
初めて変な値に気づくのではなく


事前にテキストエディタ上で、
コンパイルエラーで気づきたいですよね?


Elmという言語の場合は、

Elm
int = String.toInt("5")

↑このようにして文字列を数値に変換します。


でも、直接Int型の値には変換できません


Elm
int = String.toInt("5")

↑この関数の戻り値はMaybe Int型になります。


文字列から数値に変換する訳なので──


できないこともあります


数値に変換できない例

Elm
int = String.toInt("あああ")

なのでMaybe Int型な訳です


Maybe Int型の値は、
そのまま計算に使ったりできません


数値に変換できてるかもしれないし──


できていないかもしれない──


そんな「シュレディンガーの猫」みたいな値になります


Maybe Int型の値を計算などに使いたい場合には


必ず「数値に変換できてなかった場合の処理」を
書かないといけません


例外的な値が発生しそうな処理をする際には


「例外的な値が発生したらどうするか」を書いておかないと


コンパイルエラーが起こって先に進めません


「全てを予測可能にしたい」


「例外的な値も、事前に予測して対処したい」


そんな「関数型っぽさ」がよく現れた型ですね


こちらも、詳しくは「Elm Guide」を読んでみてください


fp-ts の話に戻ります


fp-ts のOption<T>も、先ほどのMaybeと同じような型です


「Option」という言葉には
「必須ではない」という意味があります


つまり「存在しないかもしれない値」を型で表現できます


文字列を数値に変換する例を見てみましょう


fp-ts にはfromStringという関数が用意されています


文字列を数値に変換するための関数です


fp-ts
import { fromString } from 'fp-ts-std/Number'
import { Option } from 'fp-ts/Option'

const num: Option<number> = fromString("5")

この関数はOption<number>型の値を返します


Option<number>型の値は
Maybeと同じように、そのまま計算には使えません


fromStringという関数の実装はこんなイメージです

fp-ts
import { Option, none, some } from 'fp-ts/Option'

const fromString = (value: string): Option<number> => {
    const num = Number(value)
    // NaNになっていないかチェック!
    return Number.isNaN(num) ? none : some(num)
}

ちゃんと数値に変換できたかどうか、
NaNが発生してないかどうか


そこをチェックしてくれます


そして「数値に変換できてなかった場合にはどうするか」を
ちゃんとコードで書かないといけません


そのため「NaNのまま計算してもうた!」
というミスが起こりません


では"5"という文字列を
数値に変換する例を見てみましょう


ここでは fp-ts の pipeという関数を使っていきます


pipe関数の例

fp-ts
import { pipe } from 'fp-ts/function'

const num: number = pipe(
    1,
    a => a * 2,
    a => a * 2,
    a => a * 2,
)

pipe関数を使うと、例えば
1という数値に、複数の関数を連続して適用できます。


では"5"という文字列を数値に変換しつつ
色々な処理を繋げてみましょう


まずは"5"という文字列を数値に変換してみましょう


文字列から数値に変換・・・?

fp-ts
import { pipe } from 'fp-ts/function'
import { fromString } from 'fp-ts-std/Number'

const num: number = pipe(
    "5",
    fromString,
)

// -> コンパイルエラー!

このコードは、コンパイルエラーが起こります


文字列から数値への変換は、失敗するかもしれないからです


「数値に変換できなかったらどうするか」を書かないと、
number型の変数には代入できません


Option<number>のままでは、
number型の変数には代入できません


「数値に変換できなかったらどうするか」を明記

fp-ts
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
import { fromString } from 'fp-ts-std/Number'

const num: number = pipe(
    "5",
    fromString,
+   O.match(
+     () => { throw new Error("不正な値です!") }, // 変換できなかったケース
+     (value) => value, // 変換できたケース
+   )
)

「不正な値のときには例外を投げる」と明記することで、number型に代入できました


TypeScriptのNumber()関数だと

戻り値がNaNかもしれないのに、型としてはnumberになるため、
そのままNaNを使った計算をしてしまう恐れがあります。
コンパイルエラーで気づくことができません。


fp-tsの Option<T> 型を使うと

戻り値の型をOption<number>にできるため、
そのまま計算などができません。
型エラーで事前に気づくことができます。


実行時に初めて
「画面にNaNて表示されてるやん!」
と気づくのではなく


テキストエディタ上で
「お、Option<number>やから」
「そのまま計算はできひんのやな」
と気づくことができます


「数値に変換できてなかった場合の処理も」
「ちゃんと書いとかんとな!」
と気づくことができます


また、手動でNaNチェックするよりも
「他の関数と組み合わせやすい」です


文字列から数値に変換したあと
「さらに二乗する」例を見てみましょう


文字列を数値に変換して、さらに二乗する例

fp-ts
+ // 数値を二乗してくれる関数
+ const square = (value: number): number => value * value

const num: number = pipe(
    "5",
    fromString,   // 文字列から Option<number> に変換!
+   O.map(square), // square関数を使って、値を二乗!
    O.match(
      () => { throw new Error("不正な値です!") },  // 途中で失敗したケース
      (value) => value, // 上手く行ったケース
    ),
)

square関数は、数値にしか適用できません


でもO.map()関数を使うことで、
Option<number>に対しても適用できるようになります


「存在しないかもしれない数値」を二乗するようなことが
できます・・・!


不思議ですね・・・!


O.map()関数が、
square関数を変身させてくれるからです


number用の関数を、Option<number>用の関数に
変身させてくれます


これを「関数を持ち上げる」なんて言います


「数値に変換できてないかもしれない値」に対しても
「数値用の関数」を適用できる──


そんな便利な仕組みが用意されているわけですね


でも、まだ先ほどのコードはイマイチです


さらに「Errorが発生する可能性があるかどうか」も
型で扱えるようにして行きます


そのためにEither<A, B>型を使ってみましょう


これは AB のどちらか一方の値を持つ型です


今回は「numberまたはError」ということを
型で表現してみましょう


「エラーかもしれないし、普通の値かもしれない」
そんな値を返す関数

fp-ts
import { pipe } from 'fp-ts/function'
import * as O from 'fp-ts/Option'
import { Either, left, right } from 'fp-ts/Either'

const optionToEither = <T>(optionValue: O.Option<T>): Either<Error, T> => pipe(
    optionValue,
    O.match(
        () => left(new Error("不正な値です!")),
        (value) => right(value),
    ),
)

EitherOption のように
どちらの値をとるかで分岐する必要があります


「エラーだった場合、そうじゃなかった場合」を明記しないと
コンパイルが通りません


では「エラーになるかもしれない」処理を繋げてみましょう


まず、文字列を数値に変換しつつ二乗して
「エラーかもしれない」値を作成

fp-ts
import * as E from 'fp-ts/Either'

const errorOrNum: E.Either<Error, number> = pipe(
    "5",
    fromString,    // string -> Option<number>
    optionToEither, // Option<number> -> Either<Error, number>
    E.map(square),  // Either<Error, number> -> Either<Error, number>
)

ここでもE.map()を使って
square関数を変身させています


「エラーかもしれない値」に対しても
「数値用の関数」を適用できています


そして、値に応じたメッセージを作成

fp-ts
const message: string = pipe(
    errorOrNum,
    E.match(
      (left: Error) => left.message, // 途中でエラーが起きたケース
      (right: number) => `2 乗した値は ${right} です`, // ちゃんと計算できたケース
    ),
)

console.log(message)

ここでも、エラーが起きた場合の処理を明記しないと
コンパイルが通りません


このように fp-ts をうまく活用することで


undefinedNaN、例外をとるかもしれない値などを
型に落とし込むことができます


型的に矛盾のあるコードを書いている場合は
コンパイルエラーで気づくことができます


「テキストエディタ上で、事前に気づける」という
TypeScriptのメリットを、より強化できました


しかも「存在しないかもしれない値」が発生しても、
その後の計算を繋げることができています


「全てを予測可能にしたい」


「例外的な値も、事前に予測して対処したい」


「しかも、いろんな処理を便利に繋げたい」


そういった関数型の思想を、
TypeScriptにも取り込むことができました


まとめ


なんだかんだ、TypeScriptって良いと思います


JavaScriptに比べて、だんぜん型安全ですし


undefined絡みのエラーも
けっこう事前検知できるようになっています


何より、ユーザー数が多く
ライブラリやフレームワークも豊富です


Vue も React もあります


なので、TypeScriptを使いたい場面って多いです


「でも、もっと関数型言語の思想も取り入れたい」


そんなときは fp-ts 使ってみてください


※先に関数型言語を少し触ってみてからの方が
分かりやすいかもです


〜完〜


スペシャルサンクス

  • 一緒に資料を作ってくれたsiketyan
148
132
1

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
148
132