📝
Scalaのfor内包表記をneverthrowとfp-tsで書く
概略
なっとく!関数型プログラミングを読んで、for内包表記をneverthrowやfp-tsでも利用できないかと思い調べました。
neverthrowではsafeTryとジェネレータ関数、fp-tsではDoとbindを使用することで、for内包表記のような記述ができます。
for内包表記とは
複数のコレクションの要素や、複数のOption、Either、IOなどのモナドにアクセスしたい時に、flatMapの入れ子になることを防止でき、可読性を高められる記法です。
以下は、バリデーションを実行し、すべてのバリデーションが成功したらUserを作成する処理の例です。
flatMapの入れ子
validateName(name).flatMap(name =>
validateAge(age).flatMap(age =>
validateAddress(address).flatMap(address =>
validateNumber(number).map(number =>
new User(name,age,address,number)
)
)
)
)
for内包表記
for {
validName <- validateName(name)
validAge <- validateAge(age)
validAddress <- validateAddress(address)
validNumber <- validateNumber(number)
} yield new User(validName, validAge, validAddress)
このように、入れ子にならず、容易に理解することができるのが、for内包表記の良い点です。
neverthrowの場合
neverthrowでは、safeTry関数とジェネレータ関数を使用することで実装できます。
validate関数の定義(Result)
//引数を受け取り、Resultを返します。
//その他の関数も同じような実装です。
const validateName = (name) => {
if(name.length > 0) return ok(name)
return err(`validation error!`)
}
for内包表記(Result)
safeTry(function*() {
const validName = yield* validateName(name)
const validAge = yield* validateAge(age)
const validAddress = yield* validateAddress(address)
const validNumber = yield* validateNumber(number)
return ok({
validName,
validAge,
validAddress,
validNumber
})
}).map(/** 処理が続く */)
neverthrowでは、戻り値を自動的にokでラップしたりはしないようです。そのため、自身でラップする必要があります。
また、ResultAsyncの場合はジェネレータ関数をasync関数にするだけです。
validate関数の定義(ResultAsync)
//引数を受け取り、ResultAsyncを返します。
//その他の関数も同じような実装です。
const validateName = (name) => {
if(name.length > 0) return okAsync(name)
return errAsync(`validation error!`)
}
for内包表記(ResultAsync)
//関数にasyncをつけるだけ
safeTry(async function*() {
const validName = yield* validateName(name)
const validAge = yield* validateAge(age)
const validAddress = yield* validateAddress(address)
const validNumber = yield* validateNumber(number)
//ResultAsyncを返す
return okAsync({
validName,
validAge,
validAddress,
validNumber
})
}).map(/** 処理が続く */)
fp-tsの場合
fp-tsでは、Doとbind関数を使用することで実装できます。
validate関数の定義(Either)
//引数を受け取り、Eitherを返します。
//その他の関数も同じような実装です。
const validateName = (name) => {
if(name.length > 0) return either.right(name)
return either.left(`validation error!`)
}
for内包表記(Either)
pipe(
either.Do,
either.bind("validName", validateName(name)),
either.bind("validAge", validateAge(age)),
either.bind("validAddress", validateAddress(address)),
either.bind("validNumber", validateNumber(number)),
either.map(({validName, validAge, validAddress, validNumber}) => /** 処理が続く */)
)
fp-tsでは、Doでfor内包表記を始め、bindを使うことでアクセスします。bindの第一引数の名前が変数名になり、定義した変数がオブジェクトの中に入っています。
また、TaskEitherの場合はeitherをtaskEitherに変更するだけです。
validate関数の定義(TaskEither)
//引数を受け取り、TaskEitherを返します。
//その他の関数も同じような実装です。
const validateName = (name) => {
if(name.length > 0) return taskEither.right(name)
return taskEither.left(`validation error!`)
}
for内包表記(TaskEither)
pipe(
taskEither.Do,
taskEither.bind("validName", validateName(name)),
taskEither.bind("validAge", validateAge(age)),
taskEither.bind("validAddress", validateAddress(address)),
taskEither.bind("validNumber", validateNumber(number)),
taskEither.map(({validName, validAge, validAddress, validNumber}) => /** 処理が続く */)
)
まとめ
Scalaのfor内包表記をneverthrowとfp-tsで実現する方法を解説しました。
for内包表記はネストを解消し、コードを読みやすくします。typescriptで関数型プログラミングを行う際には、ぜひ活用してみてください。
Discussion