♻️

F#で Async<'T> list を Async<'T list> に変換して扱う方法

に公開
2025/12/23

本記事は、Uzabase Advent Calendar 2025 23日目の記事です。

導入

以下のようなシグネチャの関数を書いていたとします。

let getCompanies
    (fetchCompany: CompanyId -> Async<Company>)
    (companyIds: CompanyId list)
    : Async<Company list>>
    = failwith "TODO"

この関数の目的は、企業IDのリストに対応する企業情報を、与えられた非同期取得処理(IO)で順に取得し、企業のリストとしてまとめて返すことです。

1つめの引数としてfetchCompanyという企業IDを受け取り、Async<Company>を返す関数を2つ目の引数で企業IDのリストを受け取り、Asyncで包まれたCompanyのリストを受け取りたいです。

単純に以下のようにList.mapでfetchCompanyを呼び出すだけだと、Async<List<Company>>が欲しいのに、List<Async<Company>>になってしまいます。

let getCompanies
    (fetchCompany: CompanyId -> Async<Company>)
    (companyIds: CompanyId list)
    : Async<Company list> =
    async {
        let! company = companyIds |> List.map fetchCompany
        // List<Async<Company>>をAsync<List<Company>>にしたい

        return company
    } 

どうするとよいか

FSharpPlusのsequenceを利用すると以下のように書くことができます。

open FSharpPlus

let getCompanies
    (fetchCompany: CompanyId -> Async<Company>)
    (companyIds: CompanyId list)
    : Async<Company list> =
    async {
        let! company = 
            companyIds 
            |> List.map fetchCompany 
            |> sequence

        return company
    }

また更にtraverseという関数を使うともっとシンプルに書くことができます。

open FSharpPlus

let getCompanies
    (fetchCompany: CompanyId -> Async<Company>)
    (companyIds: CompanyId list)
    : Async<Company list> =
    async {
        let! company = traverse fetchCompany companyIds

        return company
    }

なぜこう書けるのか

FSharpPlusというライブラリはHaskellなどに代表されるような関数型プログラミングの概念(Monad)などをF#で扱えるようにするライブラリです。上記のsequneceやtraverseもそういった概念(Traversable)と密接に関わり合っているのですが、今回はその部分は省いて解説していこうと思います。気になる方はぜひ調べてみてください。

sequenceの実装

まずsequenceに該当する関数を自作して見るところから始めます。
Async<'a> listを受け取り、Async<'a list>を返すような関数を書いてみます。

let sequence (x: Async<'a> list): Async<'a list> =
    failwith "TODO"
let getCompanies 
    (fetchCompany: CompanyId -> Async<Company>)
    (companyIds: CompanyId list) 
    : Async<Company list> =
    async {
        let! companies = companyIds |> List.map fetchCompany |> sequence
        return companies
    }

まず再帰関数として定義し、パターンマッチをします。

let rec sequence (x: Async<'a> list): Async<'a list>`=
        match x with
        | [] -> failwith "TODO"
        | head::tail -> failwith "TODO"  

ベースケースのパターンはAsyncで包んだ空のリストを返すようにします。

let rec sequence (x: Async<'a list>) =
  match x with
  | [] -> async.Return []
  | head::tail -> failwith "TODO" 

再帰ステップではasyncコンピュテーション式内でheadをhにバインディングすることによってリストの先頭をhにバインディングすることができるので、そのhと再帰呼出しした結果を結合させるようにします。

let rec sequence (x: Async<'a> list) =
  match x with
  | [] -> async.Return []
  | head::tail -> 
    async {
        let! h = head
        let! t = sequence tail
        return h::t
    }

これがsequenceで実現していることです。

let rec sequence (x: Async<'a> list) =
    match x with
    | [] -> async.Return []
    | head::tail -> 
        async {
            let! h = head
            let! t = sequence tail
            return h::t
        }

let getCompanies 
    (fetchCompany: CompanyId -> Async<Company>)
    (companyIds: CompanyId list) 
    : Async<Company list> =
    async {
        let! companies = companyIds |> List.map fetchCompany |> sequence
        return companies
    }

traverseの実装

先程のsequenceの実装がこちらです。

let rec sequence x =
  match x with
  | [] -> async.Return []
  | head::tail -> 
    async {
        let! h = head
        let! t = sequence tail
        return h::t
    }

これを関数が渡してリストのそれぞれの要素に関数適用ができるようにしてみようと思います。
実装はとてもシンプルで関数fを引数で受け取り、それをheadに適用させます。

let rec sequence f x =
  match x with
  | [] -> async.Return []
  | head::tail ->
    async {
        let! h = f head
        let! t = sequence f tail
        return h::t
    }

これがtraverseがやっていることです。

let rec traverse f x =
  match x with
  | [] -> async.Return []
  | head::tail -> 
    async {
        let! h = f head
        let! t = traverse f tail
        return h::t
    }

let getCompanies 
    (fetchCompany: CompanyId -> Async<Company>)
    (companyIds: CompanyId list) 
    : Async<Company list> =
    async {
        let! companies = traverse fetchCompany companyIds
        return companies
    }

また先程のsequenceは実際の実装上は異なりますが、概念的にはtraverseに恒等関数idをカリー化させたものです。

let sequence = traverse id

まとめ

ここまで色々書きましたが、実際に実装する必要はなく、FSharpPlusを活用するのがいいと思います。sequenceとtraverseが有用な場面を書いておきます。

  • sequence
    • optionやResult、Asyncのようなものでラップされた値のコレクションを持っていて、実際に必要なのはoption<'a list>やResult<'a list>, 'e>、Async<’a list>などである場合
  • traverse
    • mapとsequenceを併せて実行したい場合

参考文献

Discussion