[Scala]scalazのTaskは何が嬉しいのか


## Task?

[scalaz/scalaz](https://github.com/scalaz/scalaz)にあるクラス。  
[scalaz/Task.scala](https://github.com/scalaz/scalaz/blob/473c99f0fe07bf51a36cf5a08fa334659422f7b3/concurrent/src/main/scala/scalaz/concurrent/Task.scala)

これは[scalaz/Future.scala](https://github.com/scalaz/scalaz/blob/473c99f0fe07bf51a36cf5a08fa334659422f7b3/concurrent/src/main/scala/scalaz/concurrent/Future.scala)をwrapしたクラスとなっている。

## Future?

`scala.concurrent.Future`と同じく非同期処理を表現するクラスになっているが、  
違いとしては`map`や`flatMap`に`ExecutionContext`を必要とせず新しいタスクを生成しないようになっている点。  
これによってスレッドの再利用性が向上しているとのこと。

こんな感じ。

```scala
(for {
  i <- Future.apply(100)
  j <- Future.now(200)
  k <- Future.delay(300)
} yield i + j + k).unsafePerformAsync(println)
// 600
```

monadicなAPIを備えているのでfor式が書ける。  
`Future`自体は非同期タスクを定義・記述だけとなっていて、実行するには`unsafePerformAsync`や`unsafePerformSync`を呼び出す必要がある。

`Future`のインスタンスを作成するのに`apply`, `now`, `delay`を使っているが、`apply`だと別スレッドでの評価、`now`は正格評価で`delay`は遅延評価、みたいなイメージで使い分ける。

Scala標準の`Future`の`apply`は遅延評価となっているが、Scalazのものは異なる。  
さらに大きな違いとして例外処理機構は持っていない。  
そのため、`now`でも`delay`でも評価して例外が発生するとそれは例外になる。

```scala
Future.now { sys.error("error"); 1 }.unsafePerformAsync(println)
// Exception in thread "main" java.lang.RuntimeException: error
Future.delay { sys.error("error"); 1 }.unsafePerformAsync(println)
// Exception in thread "main" java.lang.RuntimeException: error
```

ちなみに`apply`だと闇に消える...。

```scala
Future.apply { sys.error("error"); 300 }.unsafePerformAsync(println)
// 何も表示されない
```

`Future`の実装クラスは[この辺](https://github.com/scalaz/scalaz/blob/473c99f0fe07bf51a36cf5a08fa334659422f7b3/concurrent/src/main/scala/scalaz/concurrent/Future.scala#L236-L240)にあって、例外っぽいものを表現する型はない。

じゃあ例外処理はどうするの...？となった時に`Task`が登場する。

## Task?

改めて。  
`scalaz.concurrent.Future`に例外処理を組み込んだもの。  
といいつつScala標準`Future`のようなものではなく、`Future[Throwable \/ A]`として例外を保持するようになっている。

```scala
class Task[+A](val get: Future[Throwable \/ A]) {
  ...
```  

実装は[このあたり](https://github.com/scalaz/scalaz/blob/473c99f0fe07bf51a36cf5a08fa334659422f7b3/concurrent/src/main/scala/scalaz/concurrent/Task.scala#L29)。

さっきと同じようなプログラムを`Task`で書いてみると以下のようになる

```scala
(for {
  i <- Task.apply(100)
  j <- Task.now(200)
  k <- Task.delay(300)
} yield i + j + k).unsafePerformAsync(println)
// \/-(600)
```

`Future`と同じように書けるが、結果は`\/-`になっているのが分かる。  
`Future`と違う点として、`apply`, `delay`において例外が起きるようにしてみると、`-\/`として結果を受け取る事もできるが、`now`だと引数を正格評価するのでその時点で例外が発生する。

```scala
Task.apply { sys.error("error"); 100 }.unsafePerformAsync(println)
// -\/(java.lang.RuntimeException: error)

Task.delay { sys.error("error"); 200 }.unsafePerformAsync(println)
// -\/(java.lang.RuntimeException: error)

Task.now { sys.error("error"); 300 }.unsafePerformAsync(println)
// Exception in thread "main" java.lang.RuntimeException: error
```

### Taskの嬉しいところ

#### Future的操作

Scalaの`Future`のようにコールバックや例外処理の設定を付与できる。

```scala
Task.delay { sys.error("error") }.onFinish { errOpt: Option[Throwable] =>
  Task.now {
    errOpt match {
      case Some(err) => println(s"fail! ${err.getMessage}")
      case None => println("success!")
    }
  }
}.unsafePerformAsync(println)
// fail! error
// -\/(java.lang.RuntimeException: error)

Task { sys.error("fail") }.handle { case t: Throwable => "success" }.unsafePerformAsync(println)
// \/-(success)
```
`Future`の`onComplete`と違ってコールバックが設定された`Task`が返ってくるのが地味に嬉しい。

#### 実行方法の種類

すでに上で`unsafePerformAsync`というメソッドを叩いているが、名前の通りasyncかつ危険な実行という感じ。  
`\/`で返ってくるのだからunsafeじゃないような気もするが...。

`Task`の実行方法はざっくり以下。

- `unsafePerformAsync`
  - 非同期実行で結果は`\/`に包まれる
- `unsafePerformSync`
  - 同期実行
- `unsafePerformSyncAttempt`
  - 同期実行で結果は`\/`に包まれる
- `unsafePerformSyncFor`
  - 同期実行でタイムアウト設定可能
- `unsafePerformSyncAttemptFor`
  - 同期実行でタイムアウト設定可能で結果は`\/`に包まれる

```scala
Task.apply { 100 }.unsafePerformAsync(println)
// \/-(100)
Task.apply { sys.error("fail"); 100 }.unsafePerformAsync(println)
// -\/(java.lang.RuntimeException: fail)

println(Task.apply { 100 }.unsafePerformSync)
// 100
println(Task.apply { sys.error("fail"); 100 }.unsafePerformSync)
// Exception in thread "main" java.lang.RuntimeException: fail

println(Task.apply { 100 }.unsafePerformSyncAttempt)
// \/-(100)
println(Task.apply { sys.error("fail"); 100 }.unsafePerformSyncAttempt)
// -\/(java.lang.RuntimeException: fail)

import scala.concurrent.duration._
val d = 10.millis
println(Task { 100 }.unsafePerformSyncFor(d))
// 100
println(Task { Thread.sleep(1000); 100 }.unsafePerformSyncFor(d))
// Exception in thread "main" java.util.concurrent.TimeoutException: Timed out after 10 milliseconds

println(Task { 100 }.unsafePerformSyncAttemptFor(d))
// \/-(100)
println(Task { Thread.sleep(1000); 100 }.unsafePerformSyncAttemptFor(d))
// -\/(java.util.concurrent.TimeoutException: Timed out after 10 milliseconds)
```

安全に使うなら、`unsafePerformAsync`か`unsafePerformSyncAttemptFor`のどちらかを使うことになりそう。  
ちなみにタイムアウトは`timed`メソッドでも設定できる。

```scala
Task { Thread.sleep(100); 100 }.timed(10.millis).unsafePerformAsync(println)
// -\/(java.util.concurrent.TimeoutException: Timed out after 10 milliseconds)
```

#### スケジューリングやリトライ出来る

`retry`や`Task.schedule`で設定可能。

```scala
Task { 100 }.retry(List.fill(2)(10.millis)).unsafePerformAsync(println)
// \/-(100)
Task { sys.error("fail"); 100 }.retry(List.fill(2)(10.millis)).unsafePerformAsync(println)
// -\/(java.lang.RuntimeException: fail)

Task.schedule(100, 10.millis).unsafePerformAsync(println)
// \/-(100)
```

豊富なAPIが便利。

### ScalaのFutureに変換する

実際問題、開発の基盤としてScalazいれて`Task`を全面で使っていくにしても他のライブラリとの兼ね合いで`scala.concurrent.Future`を使いたいシーンも多いはず。  
そういう時には[Verizon/delorean](https://github.com/Verizon/delorean)みたいな変換ライブラリもあるので、合わせて入れておくと便利かもしれない。[delorean/package.scala at master · Verizon/delorean](https://github.com/Verizon/delorean/blob/master/core/src/main/scala/delorean/package.scala)

## まとめ

Scalazの`Task`便利。
Scalaの`Future`と同じようなこと出来る + 便利なAPIが生えているというイメージ。
ただ、関数型っぽく明示的な実行をキックしない限り走り出さないのでそこは少し慣れが必要かもしれない。

あわせて読みたい
[ScalazのTaskを使う上で知っておきたいこと - Qiita](https://qiita.com/petitviolet/items/181e4cce35ada5cc8669)