Closed6

effect-tsのチュートリアルをやっていく

mortlackmortlack

Importing Effect

Importing Effect

Functions vs Methods

関数を使った方が、Tree Shakingの面と拡張性の面で有利である、とのこと。

  • メソッドを使った場合には、利用しないメソッドまで読み込まれてしまう

拡張性の面は、今一つ記述が簡潔すぎて、厳密な理解ができないが、
おそらくは、メソッドの場合、暗黙的にそのクラスのメンバ変数を操作する場合が多いので拡張したくなった場合に、色々考えなければならいことが多くなりがちだし、理解や方法を間違うと後々面倒なことになる、ということだと考えられる。

これはあくまでも、effectの設計方針なのだが、一般的な視点として参考になる。

mortlackmortlack

The Effect Type

The Effect Type

Effect型は、イミュータブルな値で遅延評価される。

type Effect<Success, Error, Requirements> =
    (context: Context<Requirements>) => Error | Success
  • Successをvoidにすると、値が何も生成されない、neverにすると終わらない
  • Errorをneverにするとエラーにならない
  • Requirementsをneverにすると、Context コレクションが空になる

理想的には、単一のエントリポイントで行うようにする。

副作用を実行する箇所を1カ所にまとめる。

mortlackmortlack

Creating Effects

Creating Effects

Effectは、副作用をカプセル化する計算単位である"エフェクト"を作成するための様々な方法を提供するもの。

全体的に関数型プログラミングの用語に馴染みがないと何を言っているのかなりわかりづらいかもしれない。

Why Not Throw Errors?

一般的なプログラムだとエラーがあったら、例外を投げるが、なぜ例外を投げないのか?

端的に表現すると、

  • 関数の型シグネチャと違う動きをすることになるから

関数の型シグネチャが、numberである場合、その関数のクライアントはnumber型の結果が返ってくることを期待している。しかし、その関数の中で例外を投げると、その関数は結果を返さない。それだけでなく、その例外をキャッチしている部分に処理が飛んで行ってしまう。

これだと、プログラムを読んだときに、プログラムがどう動くのかを理解しづらいので、関数シグネチャ通りに動くようにしましょう、ということ。

sync,try

同期の副作用を遅延実行できるようにする。

promise, tryPromise

非同期の副作用(promise)を扱いやすくする。

promise自体が非同期の副作用を扱いやすくしたものであるが、エラーが発生する場合、必ずしも扱いやすいわけではないため、それを改善する。

From a callback

コールバックが必要なタイプの古い?APIには、asyncを利用する

Suspended Effects

Lazy Evaluation

import { Effect } from "effect"
 
let i = 0
 
const bad = Effect.succeed(i++)
 
const good = Effect.suspend(() => Effect.succeed(i++))
 
console.log(Effect.runSync(bad)) // Output: 0
console.log(Effect.runSync(bad)) // Output: 0
 
console.log(Effect.runSync(good)) // Output: 1
console.log(Effect.runSync(good)) // Output: 2

badは、以下と等価で、紐づけした後にiの値が増える

const bad = Effect.succeed(0)

goodは、クロージャになっていて、変数が関数の内部に抱えられているので値がインクリメントされる。

Handling Circular Dependencies

import { Effect } from "effect"
 
const blowsUp = (n: number): Effect.Effect<number> =>
  n < 2
    ? Effect.succeed(1)
    : Effect.zipWith(blowsUp(n - 1), blowsUp(n - 2), (a, b) => a + b)
 
// console.log(Effect.runSync(blowsUp(32))) // crash: JavaScript heap out of memory
 
const allGood = (n: number): Effect.Effect<number> =>
  n < 2
    ? Effect.succeed(1)
    : Effect.zipWith(
        Effect.suspend(() => allGood(n - 1)),
        Effect.suspend(() => allGood(n - 2)),
        (a, b) => a + b
      )
 
console.log(Effect.runSync(allGood(32))) // Output: 3524578
  • blowsUpの方は、再帰を展開している最中にすべてがヒープに展開されるのでメモリが不足する
  • allGoodの方は、実際に1つ1つの計算行う段階で初めて展開されるので、メモリを圧迫しない

ということのようです。

Unifying Return Type

import { Effect } from "effect"
 
const ugly = (a: number, b: number) =>
  b === 0
    ? Effect.fail(new Error("Cannot divide by zero"))
    : Effect.succeed(a / b)
 
const nice = (a: number, b: number) =>
  Effect.suspend(() =>
    b === 0
      ? Effect.fail(new Error("Cannot divide by zero"))
      : Effect.succeed(a / b)
  )
  • ugly:union型が返されてしまう
  • nice:2つの型が合成された型が返される

関数型初心者の自分は、これが一番使いそう。

Cheatsheet

チートシート

mortlackmortlack

Running Effects

Running Effects

runSync

非同期の処理は実行できないので注意!(例外をスローする)

runSyncExit

同じく同期実行を行うが、ワークフローの実行結果を返す。
ワークフローの結果情報のことを一般にExitというらしい…。
初めて知りました。

runPromise

Effectを実行し、結果をPromiseとして受け取る。

runPromiseExit

runSyncExitの結果の値がPromiseのもの

runFork

同期実行やPromiseを使うなどがない場合、これを使うことが推奨される。
必要に応じて、観察、中断できるfiberが返される。

現時点では、いまいち何がうれしいのかよくわからない。

Cheatsheet

Cheatsheet

mortlackmortlack

Using Generators in Effect

Using Generators in Effect

HaskellのDoブロックみたいなものが、Effect.genとJavaScriptのジェネレータの構文を使って実現できるよう。

Understanding Effect.gen

Effect.genを使うときに従うべき要点

  • ロジックをEffect.genで包む
  • effectを扱うために、yield*を使う
  • 最終結果を返す

Comparing Effect.gen with async/await

Promiseとasync/awaitを使った場合とコードが似ている、という話。
ただ、Promiseとasync/awaitを使った場合だと、エラーハンドリングを組み込むのが面倒そう。

Embracing Control Flow

Effect.genを使うことの大きなメリットの1つは、制御文を使えること。ということだが、関数型プログラミングの観点からは、文ではなく、式で物事を表現したい気が。

Raising Errors

制御の中でエラーを扱うことができる、という説明。エラーを扱うために、Effectを使っているのだから、当たり前な気がする。

The Role of Short-Circuiting

エラーが発生した場合には、その場で即時に処理が中断される。
なんかこの説明だと、例外を発生させるように思えるけど、内部的には、Railway Oriented Programmingだと思う。

Passing this

自分自身への参照(this)を第一引数として渡すこともできる。
※できれば、クラスは使いたくない(個人の感想です)

mortlackmortlack

Building Pipelines

Building Pipelines

関数型プログラミングではおなじみのパイプラインやmapなど

Why Pipelines are Good for Structuring Your Application

なぜパイプラインを使った方がよいか

  • 可読性
  • コードの構成
  • 再利用性
  • 型安全性

が向上するから。
全くその通りなのだが、なぜそうなるのかは、Railway Oriented Programmingを理解しておいた方がよいと思う。

pipe

ここには書かれていないのでわからないが、エラーハンドリングもやってくれるものと思われる。つまり、パイプラインの先頭や中ごろにある関数がエラーを出した場合、処理は中断され結果はエラーとなる(パイプライン内のすべての関数の入出力がEffect型で行われている必要があるが)。

この前提がないと、今一つpipeの良さが伝わらない気がする。これができるので、関数1つずつの後に、エラーが起きたかどうかのif文を書かなくてよく、ただ関数をならべればよい。

処理の見通しがよくなり、パイプライン内の追加、変更、削除が容易になる。

map

不親切というか、このくらいわかれ、ということなのでしょう(サンプルをしっかり理解すればわかる)。

関数をとって、関数を適用する、という説明だけれども、これだと普通に関数を適用すればよく、なぜわざわざmapを使うのかがわからない。

このmapはいわゆる関数型プログラミングのEither型のmapに相当するもの、ということ。

例えば、Effect<Success, Error>の値があるけど、Success型の引数を取り、Effect型でない通常の値型を返す関数を適用したい、というような場合にmapを使うと便利、ということ。

処理が成功していれば、普通に関数を適用し、結果を再度Effect型へ詰めて返してくれるし、処理が失敗していた場合には、関数は適用されず、スルーされる。

as

mapの特殊なもの、と考えればよいと思います。

flatMap

mapで適用したい関数が、Effect型を返す場合、結果がEffect<Effect<>>型になってしまってこまるので、利用する。

本当はもっといろいろあるのだろうが、ここではこれだけ。

andThen

mapとflatMapの使い分けが面倒なので、与えられた関数に応じていい感じにしてくれる関数。
可読性という点で、どうなんだろう。

tap

パイプラインに戻り値がvoidの関数を挟んで、次の関数にはtapの前の値を連携する。
ログやその他の副作用や別の処理をパイプラインの途中に挟みたいときに利用する。

デバッグの時に便利そう。

all

複数のEffectをtupleやiterableやstructやrecordにできるらしい。

The pipe method

あらゆるEffectからメソッド形式で、pipeが呼び出せる。
関数形式でいいかな、と思う。

Cheatsheet

Cheatsheet

このスクラップは5ヶ月前にクローズされました