📝

Scalaのfor内包表記をneverthrowとfp-tsで書く

に公開

概略

なっとく!関数型プログラミングを読んで、for内包表記をneverthrowfp-tsでも利用できないかと思い調べました。

neverthrowではsafeTryとジェネレータ関数、fp-tsではDobindを使用することで、for内包表記のような記述ができます。

for内包表記とは

複数のコレクションの要素や、複数のOptionEitherIOなどのモナドにアクセスしたい時に、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では、Dobind関数を使用することで実装できます。

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の場合はeithertaskEitherに変更するだけです。

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内包表記をneverthrowfp-tsで実現する方法を解説しました。

for内包表記はネストを解消し、コードを読みやすくします。typescriptで関数型プログラミングを行う際には、ぜひ活用してみてください。

Discussion