F#で Async<'T> list を Async<'T list> に変換して扱う方法
本記事は、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