Runner in the High

技術のことをかくこころみ

実装と処理の分離でfor-comprehensionを使う

前回の記事の続きです

izumisy.work

前の記事だと継続渡しのスタイルで処理を表現していたが、継続渡しだとシンプルにインデントがどんどん深くなっていくという微妙な点がある。

Scalaにはfor文というのが用意されているので、継続渡しスタイルよりもfor文を使うスタイル(for-comprehension)のほうが一般的に好まれる。

実際のコードで言うと、以下のようにfor文で処理の流れを書いていく感じになる。前回の記事同様、この時点では具体的な実装に関するものは何も出てこない。あくまで処理の流れだけである。

object Main extends App {
  val program: Runnable = ???

  for {
    _ <- program.output("hello!")
    _ <- program.output("what's your name?")
    name <- program.input
    _ <- program.output(s"$name, nice to meet you.")
  } yield name
}

program変数は、実際にoutputやinputを実行するためのインタプリタの実装となる。

Runnableインタフェースはこんな感じ。

trait Runnable {
  def succeed[A](value: A): Application[A]
  def input: Application[String]
  def output(line: String): Application[_]
}

上のRunnableインタフェースが依存しているApplication型の実装は以下。

sealed trait Application[A] {
  def map[B](f: A => B): Application[B] =
    flatMap(a => Return(() => f(a)))

  def flatMap[B](f: A => Application[B]): Application[B] =
    this match {
      case Return(value) =>
        f(value())
      case Output(value, next) =>
        Output(value, next.flatMap(f))
      case Input(next) =>
        Input(a => next(a).flatMap(f))
    }
}
final case class Return[A](value: () => A) extends Application[A]
final case class Output[A](line: String, rest: Application[A]) extends Application[A]
final case class Input[A](rest: String => Application[A]) extends Application[A]

for-comprehensionを使うためにmapとflatMapを実装している。

最後にRunnableインタフェースを満たす具体的なコンソールの実装に依存したConsoleクラスを作る。

class Console extends Runnable {
  import scala.io.StdIn

  override def succeed[A](value: A): Application[A] =
    Return(() => value)

  override def input: Application[String] =
    succeed(StdIn.readLine())

  override def output(line: String): Application[_] = {
    println(line)
    Output(line, succeed(()))
  }
}

最後に、Consoleクラスを実装として差し込む。

val program: Runnable = new Console

これでfor文で処理の流れを記述しつつ処理と実装を分離することができた。

こういうのをFreeモナドって言うのかな?

継続渡しスタイルを用いた実装と処理の分離

ほぼZIOのbackgroundセクションにあるサンプルそのものだが、備忘録的に残しておく

zio.dev

いわゆるfor-comprehensionを使わずに、Applicationをミクスインした各ADTがそれぞれrestという名前で次の処理をコールバックとして取る実装(継続渡しスタイル)になっている。

sealed trait Application[A]
final case class Return[A](value: () => A) extends Application[A]
final case class Output[A](line: String, rest: Application[A]) extends Application[A]
final case class Input[A](rest: String => Application[A]) extends Application[A]

object Main extends App {

  // ここではApplicationを用いて"どのような流れで処理が行われるか"だけを実装している
  // OutputやInputは抽象化された入力と出力になっていて、ここでは実装のことを気にしない。
  val program: Application[String] =
    Output("what's your name?",
      Input(name =>
        Output(s"Nice to meet you $name",
          Return(() => name)
        )
      )
    )

  // Application型がどのように実行されるかがここに実装されている。
  // この関数はprintlnやStdIn.readLineなどコンソールの実装に依存している
  // 異なる実装に差し替える場合には、同じくApplication[A]を取るinterpreterを実装すればよい。
  def consoleInterpreter[A](program: Application[A]): A =
    program match {
      case Return(value) =>
        value()
      case Output(value, next) =>
        println(value)
        consoleInterpreter(next)
      case Input(next) =>
        consoleInterpreter(next(StdIn.readLine()))
    }

  // ここで処理と実装をがっちゃんこして初めてアプリケーションを起動する
  consoleInterpreter(program)

}

実装(consoleInterpreter)と抽象(program)が分離されていることで、差し替え可能なコードになっていることが分かる。

Scalaでモナドとかに入門できそうな記事まとめ

会社でScalaを使っているので、できれば自分もモナモナしていきたいという気持ちがある。最近見つけた参考になりそうな記事を集めてみる。

Readerモナド

Readerモナドと関数表現の比較、そして最後にFreeモナドに至る。 qiita.com

Freeモナド

medium.com

softwaremill.com

dzone.com

izumisy.work

Interpreterパターン

厳密にはFreeモナドとは関係ないかもしれないが

www.youtube.com

1ファイルでつくるFreeモナドのサンプル

Scalazとかcatsとか使わずにモナドを手作りしていてイイ

Simple Scala example of a pure functional program that does I/O · GitHub

Closeモナド

mentalpoker.blogspot.com

qiita.com

Stateモナド

rcardin.github.io

CloseモナドからStateモナド

qiita.com

継続モナド

kazuhira-r.hatenablog.com

qiita.com

Transactionモナド

ScalikeJDBCのトランザクションインターフェイス化するのがわかりやすくておもしろい

Scalaで最強のRepositoryパターンを実装する ~①howとwhatの分離~ - Qiita

Scalaで最強のRepositoryパターンを実装する ~②トランザクションモナド~ - Qiita

Scalaで最強のRepositoryパターンを実装する ~③ScalikeJDBCによる実装~ - Qiita

Tagless Final

モナドじゃないけど

qiita.com

blog.softwaremill.com

fringeneer.hatenablog.com

ZIOの作者の人の記事。Tagless-finalスタイルを紹介しつつ欠点も。 degoes.net

ZIO

こちらもモナドじゃないけど

qiita.com

分散モノリスに対するアプローチ

f:id:IzumiSy:20200416230254p:plain
分散モノリス

モノリスからマイクロサービスに至る過程でよくある分散モノリスをどのように分割していくか。

いくつかアプローチを考えたので整理してみた。

1. 内部通信

f:id:IzumiSy:20200416230338p:plain
内部通信を用いた分割
たぶん一番よくあるやつ。RPCやHTTPリクエストなどでアプリケーション間の通信を行う。

この場合には必ずしもアプリケーション毎に固有のDBを持つ必要はない。

しかし、この設計ではAP1の可用性がAP2の可用性に実質依存しているため、AP2がAP1の障害点となってしまっている。データベース自体は分離できているものの、サービス毎の可用性は低い。

2. イベント駆動

f:id:IzumiSy:20200416230627p:plain
イベント駆動

キューやPub/Subなどを利用して非同期的にデータを受け取り、逐次的にサービス毎の固有DBにデータを永続化していくパターン。内部通信とは異なり、別のサービスの可用性に影響を受けない。

しかし一方で、すでに稼働中のアプリケーションにサービスインする場合、異なるサービスのデータが必要であれば予めデータマイグレーションなどを行っておく必要がある。したがって、サービス間のデータ整合性を保つためにはトラフィックが多いタイミングでのサービスインを避けるなど、リリースのタイミングに気を使う必要がある。

3. 内部通信+イベント

f:id:IzumiSy:20200416230757p:plain
内部通信+イベント

イベント駆動で逐次的に固有のDBへデータの永続化を行いつつ、データが存在しない場合には内部通信でデータの問い合わせを行う。

例えばAP1のサービス上にデータが存在しない場合の具体的なフローは以下。

f:id:IzumiSy:20200416235328p:plain
固有のDBにデータがない場合のフロー

一度永続化が完了すれば、あとはイベント駆動のフローに乗せることができるため、イベント駆動で問題となるサービスインのタイミングも気にする必要がない。また、内部通信だけの場合と比べて可用性も担保できる。しかし一方で、データの更新をどのタイミングで行うかが課題になる。

dry-rbの作者による「サードパーティのgemを安全に使う方法」

Railsやってる人たちってドメイン層にvirtusとかinteractorとかサードパーティのライブラリが現れるのってどう思ってんの」という雑なツイートに対してROM.rbdry-rbシリーズの作者であるPiotr Solnicaが優しくリプをくれた。

絶対に守らねばならないルール、それは「常に固有のインターフェイスに依存する」であるとのこと。

いくらシンプルなGemであっても、必ずGemのインターフェイスとなるラッパーを用意すること。ラッパーを用意することで、そのGemの機能を使う側のモジュールは、ラッパーのインターフェイスのみに依存させることができる。そうすればGemがdeprecatedになっても、容易に自前で作ったものに差し替えたり、代替となるGemで入れ替えたりできる。

これは本当に間違いない。この観点から言えば、ラッパーとなるオブジェクトはできるだけサードパーティのGemの機能を抽象化したインターフェイスとなるべきだ。しかし、どこまで未来の設計を考えて抽象化するか、というのはすごく匙加減が難しいところではある。

dry-rbの作者としては、dry-rbシリーズはunobstrusive(邪魔にならない)ようにデザインしているらしい。

具体的に「どう邪魔しない」のかは分からないが、最後にSolnicaが言うように「ジェネリックな機能」だけを提供することで、dry-rbそれ自体がドメインそのものにならないようにしているのだとは思う。

たしかに、サードパーティのGemが現れるとしても、それがよほど突飛なインターフェイスを提供していない限り、用意に別のGemに乗り換えることが可能だし、仮に突飛だとしてもラップして自前のインターフェイスによる利用だけに限定してしまえばある程度は依存をコントロールできるだろう。

この手の依存をコントロールする系の話は「オブジェクト指向設計実践ガイド」にも書かれていた気がする。

ElmでPromise.all的なことをしたいときに便利なelm-task-parallel

github.com

背景

JavaScriptだと「起動時にサーバーへA, B, Cのデータの取得を問い合わせて全てデータがそろったら次の処理へ移る」というような実装をPromise.allで作ることがよくある。

これをElmで雑にやろうとすると以下のようなMaybeまみれのコードが生まれたりする。

-- Maybeでデータがロードされいているかいないかを表現している。
-- すべてJustになったら次の処理をする。

type alias Model =
    { user : Maybe User
    , posts : Maybe Posts
    , favorites : Maybe Favorites
    }

こうなるとupdate関数の中でデータを取り出すたびに毎度パターンマッチをしなければいけなくなり、とても冗長なコードになってしまう。また、毎回すべてのデータがJustかどうかをチェックしないといけなくなったりする。これは事故る可能性も高い。

可能であれば「全てのデータがそろっている/いない」を型で表現できるのが理想である。

elm-task-parallelを使う

ここでelm-task-parallelが使える。

まずはModelをこんな感じで設計する。この時点でひとつもMaybeが出てこない。

type Model
    = Failed
    | Loading (Task.Parallel.State3 Msg User Posts Favorites)
    | Loaded User Posts Favorites

Loadingはデータをロード中であることを表現している。同時に取得したいデータの型の数に合わせて、使うStateN型を変更する。ここではUser, Posts, Favoritesという3つのデータを取得待ちするのでState3を使っている。

上のModelが用意できたので、取得の開始を行うinit関数を実装する。attemptNという関数が公開されているので、同時に実行したいTaskの数に応じて数字を変えて呼び出す。なお、最大で9個のタスクまで扱うことができる。

init : () -> ( Model, Cmd Msg )
init _ =
    Task.Parallel.attempt3 
        { task1 = Api.fetchUser 
        , task2 = Api.fetchPosts 
        , task3 = Api.fetchFavorites 
        , onUpdates = LoadingUpdated
        , onFailure = LoadingFailed
        , onSuccess = LoadingFinished
        }
        |> Tuple.mapFirst Loading

続いてMsg型を以下のように実装する。ここでも、Msg3となっているのは、取得を行うデータの数と同じである。

type Msg
    = LoadingUpdated (Task.Parallel.Msg3 Http.Error User Posts Favorites)
    | LoadingFailed Http.Error
    | LoadingFinished User Posts Favorites

上記のMsg型を踏まえてupdate関数を以下のように作る。

update : Msg -> Model -> ( Model, Cmd Msg )
udpate msg model =
    case model of
        Loading loadingState ->
            case msg of
                LoadingUpdated loadingMsg ->
                    Task.Parallel.update3 loadingState loadingMsg
                        |> Tuple.mapFirst Loading

                LoaadingFailed _ ->
                    ( Failed, Cmd.none )

                LoadingFinished user posts favorites ->
                    ( Loaded user posts favorites )

         _ ->
             ( model, Cmd.none )

LoadingUpdatedバリアントは、最初に一気に実行されたTaskの結果を受け取るたびに呼び出され、Loadingバリアントが持つデータを逐次更新していく。パッケージを使う側である我々は、いまどのデータが揃っているのかを気にする必要はない。

最後にLoadedFinishedバリアントが飛んでくれば、ModelをLoadedに更新するために必要なデータが全部揃う、という流れである。これでもうMaybe型を登場させる必要はない。

内部の実装はどうなっているのか

読んでみると分かるがそんなに難しいことはしていない。

例えば、3つのデータを持つState3型はこんな感じの実装になっている。

type State3 msg a b c
    = State3 (a -> b -> c -> msg) (Maybe a) (Maybe b) (Maybe c)

つまりパッケージの実装でMaybeを隠してくれているという感じである。

あとはupdateN関数の中で、失敗状態(FailedStateN)になっていない限り、ひとつづつMaybe型のデータが更新されていく、という流れになっている。

update3 : State3 msg a b c -> Msg3 a b c -> ( State3 msg a b c, Cmd msg )
update3 (State3 onSuccess a b c) msg =
    let
        next a_ b_ c_ =
            ( State3 onSuccess a_ b_ c_, Maybe.map3 onSuccess a_ b_ c_ |> toCmd )
    in
    case msg of
        LoadedA3 data ->
            next (Just data) b c

        LoadedB3 data ->
            next a (Just data) c

        LoadedC3 data ->
            next a b (Just data)

Taskモジュールのmap関数やらandThe関数を使ってつなげていくスタイルだと、どうしても逐次実行で一つづつ結果を待つことになるが、elm-task-parallelは一度Cmdに変換してElmランタイムへ投げることによって本当に並列実行が行われている。

実際にCmd.batchでTaskの実行が一気に行われているのがattemptN関数の実装から見て取れる。

attempt3 :
    { task1 : Task x a
    , task2 : Task x b
    , task3 : Task x c
    , onUpdates : Msg3 a b c -> msg
    , onSuccess : a -> b -> c -> msg
    , onFailure : x -> msg
    }
    -> ( State3 msg a b c, Cmd msg )
attempt3 { task1, task2, task3, onUpdates, onSuccess, onFailure } =
    ( State3 onSuccess Nothing Nothing Nothing
    , [ task1 |> routeTo (onUpdates << LoadedA3) onFailure
      , task2 |> routeTo (onUpdates << LoadedB3) onFailure
      , task3 |> routeTo (onUpdates << LoadedC3) onFailure
      ]
        |> Cmd.batch
    )

Scalaにおける無名クラスのメソッド呼び出しはリフレクションが使われる

こんなコードを書いた。

val App = new {
  def say = println("hello")
}

App.say

コレ自体実行はできるが、以下の警告がでる。

reflective access of structural type member method say should be enabled
by making the implicit value scala.language.reflectiveCalls visible.
----
This can be achieved by adding the import clause 'import scala.language.reflectiveCalls'
or by setting the compiler option -language:reflectiveCalls.
See the Scaladoc for value scala.language.reflectiveCalls for a discussion
why the feature should be explicitly enabled.

Scalaでは無名クラスにおけるメソッド呼び出しはランタイムリフレクションが使われるからである。

任意のトレイトを実装したクラスをワンショットで作りたい、などの用途では使われるケースが考えられるが、パフォーマンスや型安全性のことを考えると避けるほうが良いのかもしれない。

Scalaでは無名関数は型パラメータをとれない

こんなコードを書いた

object App {
  type Value[A] = Seq[(A, A)]
  
  sealed trait Status {
    def map[A](f: Value[A] => Value[A]) =
      this match {
        case Valid(value) => Valid(f(value))
        case Invalid() => Invalid()
      }
  }
  
  case class Valid[A](value: Value[A]) extends Status
  case class Invalid() extends Status
}

このコードをコンパイルすると、7行目の関数引数fを適用する部分で以下のエラーになる。

type mismatch;
 found   : Playground.App.Value[Any]
    (which expands to)  Seq[(Any, Any)]
 required: Playground.App.Value[A]
    (which expands to)  Seq[(A, A)]

これはパターンマッチにおけるValid型のAの型が決まらないからなので、mapメソッドが所属しているStatusトレイトそのものをStatus[A]みたいな形でジェネリックにすれば解決する。

一方で「なぜ無名関数をジェネリックにできないのか」は少しおもしろいトピックだと思う。これに関しては、このStackoverflowの回答でScalaの言語仕様と共に詳しく説明されていた。 stackoverflow.com

つまり、無名関数がジェネリックになれない(型パラメータをとれない)のは「メソッド」と「関数」の違いに由来するもの。

Scalaにおいてメソッドはジェネリックになれるが、関数は内部ではFunctionNとして表現されるため定義時点で型が決まりジェネリックにはなれない。したがって、無名関数もジェネリックになれないということのようだ。

「じゃあ他の言語はどうなんだ」というハナシになるが、例えばTypeScriptだと無名関数もこんな感じでジェネリックにできる。

const foo = <T>(x: T) => x;