📝
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