この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
こんばんは!突然ですが Future 使い倒してますか?
Future は、なんか中二病っぽい名前ですが、とっても便利ですよ!
一緒に Scala の Future を散歩してみませんか?
なんとなく Future
“未来”という文脈
以前、文脈を持った値 についてお話したことがあったと思います。
Future も文脈を持った値です。
Future[A]
は 「未来に存在する A という値」*1 という文脈を持っています。
などという小難しいことはとりあえずおいといて、 scala.concurrent.Future の世界を散歩してみましょう。
ブログ記事を取得する関数を作る
とりあえず、ブログサイトを作っていることにしましょうか。
findPostById という関数を用意しました。データベースから記事をひっぱってくるイカしたヤツです。
// 記事を取得する
def findPostById(postId: String): Post =
PostRepository.getPost(postId)
今回は、記事をレンダリングするためにこれを利用することにしました。
val post = findPostById("no01") // ブロッキング!
// レンダリング処理
renderPost(post)
この関数は、おおむね期待通りの働きをしてくれました。が、問題もありました。彼がデータベースから記事をひっぱってきている間、私たちはじっと待っていなければならないのです。これはいけない。
なんとかして、記事をデータベースから引っ張ってくる処理を、ノンブロッキングに行いたいですね。さて、どうしましょうか。
とりあえず、 findPostById を改良して、 Future にくるんでみることにします。
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def findPostById(postId: String): Future[Post] = Future {
PostRepository.getPost(postId)
}
処理を Future {}
でくるみました。戻り値の型も Future[Post]
に変わりました。
あと、 import 文が現れましたね。scala.concurrent.Future
のインポートはまあいいとして、次のインポートはなんだろうか。これは後で説明しますので、おまじないと思っておいてくださいな。
関数のなかみを Future にくるんだおかげで、新しい findPostById 関数は処理をブロックしなくなりました。
val futurePost: Future[Post] = findPostById("no01") // ノンブロッキング!
futurePost.onSuccess { case post =>
/* こっから未来のセカイ */
renderPost(post) // レンダリング処理
}
ノンブロッキング! いい響きです。
findPostById は Future[Post]
を返すようになりました。つまり、 「未来に存在するPost」という 文脈を持った値 を返すようになったのです。
続けて、 futurePost.onSuccess
に関数を渡しています。そして、その関数のなかでレンダリング処理を行っています。
ここで渡した関数には、未来の世界で行われる処理を記述するんですね。
つまり、futurePost が成功裏に記事(post)を取得できた場合のアルファ世界線での出来事について記述しておく … と。
14歳の夏を偲びながら書くとこうなりますが、ようは処理が成功した場合のコールバックです。
先ほどインポートしたおまじないの scala.concurrent.ExecutionContext.Implicits.global
は、アルファ世界線で処理を実行してくれる人の指名です。ここで指名した人が責任をもって渡した関数を実行してくれます(アルファ世界線で)。
さあこれで私たちも Future マスター!明日からノンブロッキングでブイブイ言わせてやりましょう!
地獄の訪れ
…と、思っていたのですが、ちょっと困ったことになってしまいました。
まずはこの関数を見て下さい。
// ユーザーがお気に入り登録したブログ記事IDのリストを取得する
def findBookmarkListOfUser(userId: String): Future[List[String]] = Future {
BookmarkRepository.getAllByUser(userId)
}
val futureList = findBookmarkListOfUser("Clownpiece")
Await.result(futureList, Duration.Inf) // -> List(no59, no33, no29)
見ての通りの関数です。ブログに、最近はやりの ふぁぼ機能 というのを追加したので、お気に入り登録した記事IDのリストを取得するための関数を作りました。リストは、登録した日時が新しい順にソートされています。
今回、すでに完成しているこの関数を使って私たちが新たに実装しようとしている機能は 「一番最近お気に入り登録した記事を表示する」 というものです。
先ほど作った findPostById も合わせれば、必要な材料は揃っているので、実装はとても簡単でした:
val futureBookmarkList = findBookmarkListOfUser("Lilywhite")
futureBookmarkList.onSuccess { case bookmarkList =>
val recentPostId = bookmarkList.head
val futurePost = findPostById(recentPostId)
futurePost.onSuccess { case post =>
// レンダリング処理
renderPost(post)
}
}
はい。できるにはできたんですが。
なんだろう。まさか、こういうコードを Scala で書くはめになるなんて、思ってもいなかったというか。こういうコードから嫌だから Scala を書き始めたのになぁ、などと。
現時点ではまだ、不吉な悪魔の全貌までは見えていないかもしれません。しかしながら、幸運にも、かつて node.js のヘビーユーザーだった私たちは、このコードがいずれ次のような無間地獄に陥るだろうと確信できました。
futureA.onSuccess { a =>
futureB.onSuccess { b =>
futureC.onSuccess { c =>
futureD.onSuccess { d =>
...
}
}
}
}
とにかく、こんなはずではなかった。何が間違いだったのでしょうか?
文脈を持った値 Future
“文脈”のおさらい
さて、冒頭にもお伝えしましたが、Scala の Future は文脈を持った値です。
ところで、文脈を持った値とは、どんな値だったでしょうか。ちょっとおさらいです。
- 文脈を持った値すべてに定義されているflatMapメソッドを使えば、普通の値→文脈を持った値 型の関数を、文脈を保ったまま適用することができる。
// 普通の値(Int)を引数にとって、文脈を持った値(Option[String])を返す関数
def toMessage(n: Int): Option[String] =
if (n < 10) Some("the number is " + n)
else None
val maybeNumber = Some(1) // 文脈を持った値(Option[Int])
maybeNumber flatMap toMessage // -> Some(the number is 1)
val otherMaybeNumber = Some(99)
otherMaybeNumber flatMap toMessage // -> None
val anotherMaybeNumber = None
anotherMaybeNumber flatMap toMessage // -> None
- for 式で、 flatMap や map のネストを簡潔に表現できる。
for {
fstNum <- Some(4)
sndNum <- Some(7)
fst <- toMessage(fstNum)
snd <- toMessage(sndNum)
} yield fst + " and " + snd
これらの強力な機能は、もちろん Future でも利用することができます。
Future の文脈
Scala の Option の文脈、こんなかんじでした。
データ型 | 文脈 |
---|---|
Option[A] | あるかもしれないし、ないかもしれない A という値 |
さて、Scala の Future の文脈は、だいたい次のような感じです。
データ型 | 文脈 |
---|---|
Future[A] | 未来で取得できるかもしれないし、できないないかもしれない A という値 |
さっそくですが例を見てみましょう。
def toFutureMessage(n: Int): Future[String] = Future {
if (n < 10) "the future number is " + n
else throw new IllegalArgumentException
}
val futureMessage = toFutureMessage(1) // 成功の文脈
futureMessage.onSuccess { /** こっち(成功の文脈の世界線)は実行される! */ }
futureMessage.onFailure { /** こっち(失敗の文脈の世界線)は実行されない */ }
Await.result(futureMessage, Duration.Inf) // -> the future number is 1
val failingFutureMessage = toFutureMessage(99) // 失敗の文脈
failingFutureMessage.onSuccess { /** こっちは実行されない */ }
failingFutureMessage.onFailure { /** こっちは実行される! */ }
Await.result(failingFutureMessage, Duration.Inf) // -> 例外: IllegalArgumentException
上記のように、Futureは「成功の文脈」のほかに「失敗の文脈」を持つことができ、それは例外のスローで表現されます。
ところで、さっき findPostById を使って遊んでいるときに、 onSuccess
を使って アルファ世界線の出来事 について記述しましたよね。あのときのアルファ世界線とは、つまるところ 成功の文脈の先にある未来(世界線) です。私たちが実際にその未来(世界線)へと辿り着いたのならば実行されますが、そうでないならば実行されません。 onFailure
も同じく、失敗の文脈の先にある未来について記述します。そしてもちろん、私たちが失敗の文脈の先の未来に辿り着いたのならば、その未来が実行されます。
あ、もちろん flatMap も使えますよ。なんたって文脈を持った値ですからね。
def toFutureMessage(n: Int): Future[String] = Future {
if (n < 10) "the future number is " + n
else throw new IllegalArgumentException
}
val futureNumber = Future { 1 } // 成功の文脈を持った Future[Int]
val futureMessage = futureNumber flatMap toFutureMessage // -> 成功の文脈を持ったFuture[String]
Await.result(futureMessage, Duration.Inf) // -> the future number is 1
val anotherFutureNumber = Future[Int] { throw new RuntimeException } // 失敗の文脈を持った Future[Int]
val anotherFutureMessage = anotherFutureNumber flatMap toFutureMessage // 失敗の文脈を保ったままの Future[String]
Await.result(anotherFutureMessage, Duration.Inf) // -> 例外: RuntimeException
こんな感じで。*2
そしてもちろん、 for 式でも扱うことができます!
これは超オススメの書き方で、そしてたぶん最強の書き方です。この書き方を覚えればQOLがだいたい30くらい上昇するでしょう。
def toFutureNumber(n: Int): Future[Int] = Future { n }
def toFutureMessage(n: Int): Future[String] = Future {
if (n < 10) "the future number is " + n
else throw new IllegalArgumentException
}
val result = for {
number <- toFutureNumber(1)
anotherNumber <- toFutureNumber(2)
message <- toFutureMessage(number)
anotherMessage <- toFutureMessage(anotherNumber)
} yield message + " and " + anotherMessage
Await.result(result, Duration.Inf) // -> the future number is 1 and the future number is 2
すばらしい。
もちろん、失敗の文脈だって保たれますよ。
val result = for {
number <- toFutureNumber(99)
anotherNumber <- toFutureNumber(1)
message <- toFutureMessage(number) // ここで99が渡され、失敗の文脈に入る。
anotherMessage <- toFutureMessage(anotherNumber) // すでに失敗の文脈にいるので処理されない。
} yield message + " and " + anotherMessage
Await.result(result, Duration.Inf) // -> 例外: IllegalArgumentException
もう Future を文脈を持った値として扱う方法は完ぺきですね!
では、さっき問題になっていた、ブログサイトの 「一番最近お気に入り登録した記事を表示する」 にもう一度挑んでみましょうか!ところで、「一番最近」ってなんかあれですね。頭痛が痛いみたいですね。
文脈を使いこなす
地獄の打破
おさらいです。 「一番最近お気に入り登録した記事を表示する」 ために使うのは、次の2種類の関数でした。
findPostById
// 記事を取得する
def findPostById(postId: String): Post =
PostRepository.getPost(postId)
findBookmarkListOfUser
// ユーザーがお気に入り登録したブログ記事IDのリストを取得する
def findBookmarkListOfUser(userId: String): Future[List[String]] = Future {
BookmarkRepository.getAllByUser(userId)
}
そして、私たちが最初に書いた実装は次のようなものでした。
val futureBookmarkList = findBookmarkListOfUser("Lilywhite")
futureBookmarkList.onSuccess { bookmarkList =>
val recentPostId = bookmarkList.head
val futurePost = findPostById(recentPostId)
futurePost.onSuccess { post =>
// レンダリング処理
renderPost(post)
}
}
いまの私たちなら、これは次のように書くことができますね!
val futurePost = for {
bookmarkList <- findBookmarkListOfUser("Lilywhite")
post <- findPostById(bookmarkList.head)
} yield post
futurePost.onSuccess { case post =>
renderPost(post)
}
ネストがなくなりました!
さて、ところで、例えばです。例えば、新しく 「全登録ユーザーの最近のお気に入りから、ランダムで1つ記事を表示」 という機能を作る事になったとしましょうか。
// 全登録ユーザーからランダムで1つIDを取得
def getRandomUserId(): Future[String]
こんな関数を用意しました。データーベースアクセスを行いますから、ユーザーIDは Future で返されます。この関数の実装はあまり関係ないので省略しますね。
さて、この getRandomUserId を用いて、先ほどの処理を 次のように拡張すれば、それで完成です!
val futurePost = for {
userId <- getRandomUserId()
bookmarkList <- findBookmarkListOfUser(userId)
post <- findPostById(bookmarkList.head)
} yield post
futurePost.onSuccess { case post =>
renderPost(post)
}
文脈を持った値としての Future がいかに素晴らしいかわかります。
まるで、普通の値を、同期的な処理で扱っているかのように書けます。これなら拡張も変更も楽勝ですね!
ところで、上記の例で、とても残念なことに findBookmarkListOfUser が内部で処理に失敗してしまい、Future が失敗の文脈に入ってしまったとしましょう。この場合、どうなるのでしょうか。
val futurePost = for {
userId <- getRandomUserId() // 成功した! userId として "Scarlet" が取得できました。
bookmarkList <- findBookmarkListOfUser(userId) // ここで失敗した!for式は失敗の文脈に入ります。
post <- findPostById(bookmarkList.head) // すでに失敗の文脈にいるので処理されない!
} yield post
// 全体の結果として、失敗の文脈を返しました。
futurePost.onSuccess { case post =>
/* ここは成功の文脈の未来(成功した世界線)なので実行されない */
renderPost(post)
}
futurePost.onFailure { case error =>
/* ここは失敗の文脈の未来(世界線)なので、実行される */
// 何かエラー処理...
}
こんなかんじで、 for 式が返すのは 全体としての結果 です。それが失敗の文脈にいるなら、 onFailure に定めた未来が実行されます。もちろん、findBookmarkListOfUser 以外の関数 (getRandomUserId や findPostById) のみが失敗したとしても、同じように全体として失敗の文脈が返されますよ!
まとめ
- Scala の Future を文脈を持った値として扱えば、多くの課題を解決することができます。
- ノンブロッキング、コールバック地獄、例外処理、etc...
- 文脈を持った値と for 式の組み合わせは、Scala における最強の組み合わせの一つです。使いこなしましょう!
ちょっと長くなってしまいましたね。ではまた!
参考
脚注
*1 のっけからややこしいお話はしたくなかったので、失敗の文脈を考慮しないシンプルなものにしています。
*2 本来、 Future {}
にくるまれるのは非同期で実行したい時間のかかる処理ですが、今回は例なので一部をシンプルな定数値としています。そんでもって、定数値を返す場合は、 Future {}
でくるむよりも、 Future.successful
や Future.failed
に渡すほうがベターなのですが、長くなりそうなのでそれはまた次の機会に!