Dataloaderの元になったHaskellのHaxlというライブラリがおもしろい

GraphQL APIを作るとき、素朴に実装するとN+1問題が発生しがちなので、これを解消するためにDataloaderという仕組みも実装するのが一般的です。

少し前にHaskell Dayというイベントの発表資料をレビューする機会があり*1、Dataloader的な設計はGraphQL以前から知られていて、Facebookで開発されたHaxlというHaskellのライブラリに由来するということを知りました*2

ちなみにレビューした発表はこちらです(この発表をネタにして記事を書いていいですか?と発表者の@nakaji_dayoさんに聞いたところ、快諾いただけた上にこのブログ記事の内容もレビューしていただけました!ありがとうございます)。

www.youtube.com
speakerdeck.com

このHaxlですが、他言語で実装された一般的なDataloaderにはない特徴があります。

よくあるDataloaderには、RDBMSへのクエリやWeb APIの呼び出しなどのリモートへのリクエストのバッチ化や、その結果をキャッシュしておく機能があります。Haxlではこれに加えて、ApplicativeDoというGHC拡張*3を用いることで、データの取得を暗黙に並行化できます。

たとえば、データAとデータBが必要なとき、AとBに依存がなければ、ユーザーが何も指定しなくても、Aの取得とBの取得を順番に実行するのではなく、同時に実行してくれるのです。

これは特定の構文(といっても特殊なものではなく、Haskellでは一般的なdo構文という書き方)を使ってHaxlを呼び出すコードを書くと、コンパイラがコンパイル時に処理を並行に実行できるようにしてくれるという方法で実現されています。より詳しく知りたい方は、ぜひ冒頭で紹介した発表を見てください。

この嬉しさを実感するには、実際にコードをみた方が早いでしょう。

AとBの取得を並行化したい場合、JavaScriptではPromise.all、Goではgoroutineとsync.WaitGroupを使うなど、人間が手で依存を解決して、並行に実行されるコードを書く必要があります。

// 並行に取得しない場合
async function numCommonFrends(a, b) {
  const fa = await friendsOf(a);
  const fb = await friendsOf(B);
  return intersect(fa, fb).length;
}
// 並行に取得する場合
async function numCommonFrends(a, b) {
  const [fa, fb] = await Promise.all([friendsOf(a), friendsOf(b)]);
  return intersect(fa, fb).length;
}
// 並行に取得しない場合
func NumCommonFrends(a, b *User) int {
	fa := friendsOf(a)
	fb := friendsOf(a)
	return len(intersect(fa, fb))
}
// 並行に取得する場合
func NumCommonFrends(a, b *User) int {
	var fa, fb []*User
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		fa = friendsOf(a)
		wg.Done()
	}()
	go func() {
		fb = friendsOf(b)
		wg.Done()
	}()
	wg.Wait()
	return len(intersect(fa, fb))
}

ApplicativeDoとHaxlを使うと、この地味で面倒な作業をコンパイラがやってくれるわけです。

-- 何もしなくても並行に処理が実行される
numCommonFriends a b = do
    fa <- friendsOf a
    fb <- friendsOf b
    return (length(intersect fa fb))

私は普段ウェブアプリケーションの開発ではJavaScript (TypeScript) やGoを書いていて、Haskellを使うことは全くありませんが、こういうところを見るとHaskellは進んでるな〜と感じます。

こうして見比べてみると、Goのコードの冗長さが目立ちますね……。

*1:Haskellにはあまり詳しくないので、主にGraphQL面のレビューをしました。

*2:調べたらオリジナルのdataloaderのREADMEにも書いてありました。

*3:GHCはGlasgow Haskell CompilerというHaskellコンパイラのデファクトスタンダードで、GHC拡張とはコンパイラのFeature Flagを切り替えてHaskellの言語機能を拡張できる仕組みです。