1. 概要

関数は、その戻り型以外に観察可能な結果がない場合、効果がないと言われます。純粋関数とも呼ばれ、より大きなプログラムに簡単に構成できます。 構成可能性は、正しく保守可能なプログラムを作成するために不可欠です。

すぐに使えるエフェクトシステムが含まれているプログラミング言語はほとんどありません。Haskellが最も人気のある言語です。 Scalaにはエフェクトシステムがありませんが、その型システムはライブラリとして記述できるほど強力ですが、コンパイラーが適切な使用を強制しないという制限があります。

ZIO、Monix IO、 Cats Effects などの複数のライブラリは、Scalaにエフェクトシステムを提供しようとします。 このチュートリアルでは、Cats Effects 3を使用してエフェクトを制御し、コードの構成可能性を最大化する方法を学習します。

2. なぜ効果を制御する必要があるのですか?

関数型プログラミングは純粋関数と構成を優先しますが、有用なプログラムは、コンソールへの印刷やファイルへの書き込みなど、最終的には効果がなければなりません。 ほとんどのプログラムには純粋なコードと効果的なコードが混在しているため、両方のタイプのコード間の境界を制御する必要があります。 これがエフェクトシステムの仕事です。

効果によってコードが合成不可能になる理由を示す標準的な例を見てみましょう。

val tuple = (println("Launch missiles"), println("Launch missiles"))
println("--------------")
val print = println("Launch missiles")
val newTuple = (print, print)

上記のコードの出力は、printlnの呼び出しをそれが返す値に単純に置き換えることはできないことを示しています。 Scalaは最初の式を熱心に評価し、2行をコンソールに出力します。 4番目の式は1回だけ出力されます。

scala> Substitution.effectful()
Launch missiles
Launch missiles
--------------
Launch missiles

Future内のエフェクトでコードをラップすることでエフェクトを遅らせるとどうなりますか? 例を見てみましょう:

def effectfulWithFuture(): Unit = {
  implicit val ec: ExecutionContextExecutor = ExecutionContext.global 
  Future(println("Launch missiles")).map(_ => Future(println("Launch missiles")))
}

def effectfulWithFutureRefactored(): Unit = {
  implicit val ec: ExecutionContextExecutor = ExecutionContext.global
  val lauch = Future(println("Launch missiles"))
  lauch.map(_ => lauch)
}

2番目の方法は、将来の内部の効果をキャプチャしてから、それを2回使用しようとします。 しかし、ご覧のとおり、それでも1回だけ実行されます。

scala> Substitution.effectfulWithFutureRefactored()
Launch missiles

効果的な関数をその戻り値に単純に置き換えることはできないと結論付けることができます。上記の例が示すように、効果により、コードのリファクタリングが困難になります

また、実行を遅らせるだけでは、コードの代替可能性を取り戻すのに十分ではないこともわかりました。

3. 最小限のエフェクトシステムを作成する

上で述べたように、Haskellはエフェクトシステムを備えた最も人気のある言語です。 純粋でないコードを記述したい場合は、 IO 型コンストラクター内でデータを返すことにより、型で宣言する必要があります。 Haskellは遅延評価を使用するため、IO構造は何も実行しません —実行するステップを宣言するだけで、明示的に実行を強制するまで実際には何も評価されません。

前のセクションから、効果を遅らせるアプローチでは不十分であることがわかっており、 Scalaは、Haskellとは異なり、熱心に評価されています。 しかし、Scalaの call-by-name 機能のおかげで、遅延評価を手動で導入できます。

エフェクトの実行を怠惰にするクラスを作成するのは簡単です。

class LazyIO[T](val runEffect: () => T)

しかし、Scalaには私たちの生活を楽にするツールがあるので、それらを使用する必要があります。 たとえば、ケースクラスmapおよびflatMapの実装により、クラスをfor-comprehensionsで使用できるようになります。

case class LazyIO[A](runEffect: () => A) {
  def map[B](fn: A => B): LazyIO[B] = LazyIO.io(fn(runEffect()))
  
  def flatMap[B](fn: A => LazyIO[B]): LazyIO[B] = LazyIO.io(fn(runEffect()).runEffect())
}

object LazyIO {
  def io[A](effect: => A): LazyIO[A] = new LazyIO[A](() => effect)
}

次に、それらをモナディックコードで使用できます。

val io = LazyIO.io(println("Launch missiles"))

val twoRuns = for {
  one <- io
  two <- io
} yield (one, two)
    
twoRuns.runEffect()

このアプローチを使用して、コンソール印刷コードを構成可能にしました。 しかし、コンソールへの印刷は、プログラムによって実行される唯一の効果ではなく、最も重要でもありません。

4. 猫の効果を入力してください

ほとんどすべての興味深いプログラムには、単にコンソールに印刷するよりも多くの効果があります。 たとえば、プログラムはリソースを取得し、エラーを処理し、タスクを並行して実行する場合があります。

エフェクトシステムは、純粋で効果的なコードを変換して組み合わせることにより、さまざまな種類のエフェクトを処理し、より大きなプログラムを宣言する方法を提供する必要があります。 そして最後に、宣言されたすべての効果を実行するプログラムを実行する方法を提供するはずです。 エフェクトを実行するために使用されるメソッドは、エリミネーターとして知られています。

Cats Effectsは、それ以上のものを提供してくれます。

4.1. プロジェクトの依存関係に猫の効果を追加する

プロジェクトに依存関係を追加することから始めなければなりません。 SBTでは、build.sbtに行を追加するだけです。

libraryDependencies ++= Seq(
  "org.typelevel" % "cats-effect_2.12" % "2.1.4"
)

Scala App の代わりが含まれていることに注意してください。これにより、アプリケーション全体を単一のエフェクトとして記述できます。

import cats.effect.{ExitCode, IO, IOApp}

object MissileLaucher extends IOApp {
  def putStr(str: String): IO[Unit] = IO.delay(println(str))
  
  val launch = for {
    _ <- putStr("Lauch missiles")
    _ <- putStr("Lauch missiles")
  } yield ()
  
  override def run(args: List[String]): IO[ExitCode] = launch.as(ExitCode.Success)
}

他のJavaアプリケーションを実行するのと同じ方法でIOAppを実行できます。

4.2. 基本的なコンビネーター

前の例では、モナディック構文を使用して IO オブジェクトを使用しました。これは、IOmapflatMapの両方を実装していることを示しています。 。 ただし、 Cats と組み合わせると、traverseやsequenceなどのコンビネーターにアクセスできます。

import cats.effect.{ExitCode, IO, IOApp}
import cats.implicits._

object TraverseApp extends IOApp {
  def putStr(str: String): IO[Unit] = IO.delay(println(str))

  val tasks: List[Int] = (1 to 1000).toList
  def taskExecutor(i: Int): String = s"Executing task $i"
  val runAllTasks: IO[List[Unit]] = tasks.traverse(i => putStr(taskExecutor(i)))
  
  override def run(args: List[String]): IO[ExitCode] = runAllTasks.as(ExitCode.Success)
}

object SequenceApp extends IOApp {
  def putStr(str: String): IO[Unit] = IO.delay(println(str))

  val tasks: List[IO[Int]] = (1 to 1000).map(IO.pure).toList
  val sequenceAllTasks: IO[List[Int]] = tasks.sequence
  val printTaskSequence = sequenceAllTasks.map(_.mkString(", ")).flatMap(putStr)
  
  override def run(args: List[String]): IO[ExitCode] = sequenceAllTasks.as(ExitCode.Success)
}

上記の例では、2つの異なるコンストラクターを使用しました。 IO.delayは最も一般的に使用されるものであり、効果を遅らせることによってIOを構築します。 もう1つはIO.pureで、純粋な値をIOエフェクトにラップする必要がある場合に使用します。

ひと目で、 トラバース順序彼らは似ているように見えるので少し些細なように見えます地図 flatMap。 ただし、これらは、インスタンスを持つ任意のオブジェクトで機能する一般的な用語で定義されています。 トラバース型クラス。

4.3. 並列実行

Java仮想マシンでは、並列実行とは、コードが異なるスレッドで実行されていることを意味します。 理想的には、実行速度がコードを並列実行するか順次実行するかの唯一の違いですが、これにより、コードが並列実行されているかどうかを判断するのが困難になります。

任意のIOを拡張する暗黙クラスを記述して、実行中のスレッドを表示できます。

object Utils {
  implicit class ShowThread[T](io: IO[T]) {
    def showThread: IO[T] = for {
      thunk <- io 
      thread = Thread.currentThread.getName
      _ = println(s"[$thread] $thunk")
    } yield thunk
  }
}

このユーティリティを使用すると、デフォルトのtraverseが並行して実行されないことを示すことができます。

[ioapp-compute-0] 1
[ioapp-compute-0] 2
[ioapp-compute-0] 3
[ioapp-compute-0] 4
[ioapp-compute-0] 5
[ioapp-compute-0] 6
[ioapp-compute-0] 7
[ioapp-compute-0] 8
[ioapp-compute-0] 9
[ioapp-compute-0] 10
[ioapp-compute-0] List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11)

しかし、Cats Effectsを使用すると、1行を変更することで並行して実行できます。 parTraverseを使用して、IOにエフェクトを並列に実行するように依頼します

object ParallelApp extends IOApp {
  val tasks: List[IO[Int]] = (1 to 10).map(IO.pure).map(_.showThread).toList

  val incremented: IO[List[Int]] = tasks.parTraverse {
    ioi => for (i <- ioi) yield i + 1
  }

  val parallelOrNot = incremented.showThread

  override def run(args: List[String]): IO[ExitCode] = parallelOrNot.as(ExitCode.Success)
}

そして、私たちが書いたユーティリティは、それが実際に異なるスレッドで実行されることを示しています。

[ioapp-compute-2] 3
[ioapp-compute-1] 2
[ioapp-compute-6] 7
[ioapp-compute-3] 4
[ioapp-compute-8] 9
[ioapp-compute-4] 5
[ioapp-compute-0] 1
[ioapp-compute-7] 8
[ioapp-compute-9] 10
[ioapp-compute-5] 6
[ioapp-compute-9] List(2, 3, 4, 5, 6, 7, 8, 9, 10, 11)

5. 結論

このチュートリアルでは、エフェクトシステムとは何か、なぜそれが役立つのかを学びました。 また、アプリケーションをIOAppとして作成する方法の基本についても確認しました。

また、効果としての並列処理のモデリングについても学びました。これにより、並列で実行しながら、parMapなどの高レベルのコンビネーターを使用して理解できるプログラムを作成できます。

いつものように、例の完全なソースコードは、GitHubから入手できます。