Runner in the High

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

Elmのモジュール分割における区分軸の観点

Elmアプリケーションにおいてモジュールを作る際に、モジュールへ機能をどう凝集させるかに関して。

機能による区分

かつてElmにおいて一般的だった分割方針。グレーの部分がひとつのモジュールを表している。

ModelとUpdate, Viewなどをすべて別のモジュールへ分割し、Userなどのネームスペース配下に配置する形で、Railsなどのフルスタックフレームワークディレクトリ分けと非常に近い。

f:id:IzumiSy:20200517012328p:plain
機能による区分

この分割方針の大きな欠点は、モジュールを不必要な単位で分けすぎていることで結局すべてのデータをモジュールから公開しなければならなくなっている点にある。

ViewモジュールやUpdateモジュールは、実装のためにModelモジュールの内部実装を知る必要がある。その結果、公開された内部実装へ知る必要のない他のモジュールが依存する可能性がでてしまう。

Elmにおいてモジュールを作る理由というのは公開したいものと公開したくないものを分けることであり、他モジュールからの依存をコントロールし変更に強い「インターフェースに対する依存」を生み出すこと。Opaque Typeのような手法も、まさにこれが理由になる。

モジュールの分け方が不適切な単位で小さすぎると、実装のためにモジュール内部をすべてexposingしないといけなくなってしまいモジュールの機能が存在意義を失ってしまう。このような「機能による区分」が持つ問題への反省を踏まえて、現在のElmにおいては以下の「ドメインによる区分」の方針でモジュール分割が行われる。

ドメインによる区分

「機能による区分」で行われていた分割とは異なり、モジュールはUser, Article, Commentのようなアプリケーションにおけるドメインの単位のみで作られる。

最も卑近な例では、リチャード・フェルドマンによるrtfeldman/elm-spa-exampleがモジュール分割にこの区分を採用している。

f:id:IzumiSy:20200517012408p:plain
ドメインによる区分

このモジュール分割が行われるのは、例えば「画面」がそうである。Elmアプリケーションにおいて、User画面、Article画面、Comment画面はそれぞれがひとつづつモジュール化される。

画面をモジュール化する理由は、画面を利用するMainなどのモジュールは各画面の実装詳細を知る必要がないからだ。これらのモジュールを利用するMainモジュールは、各画面モジュールが提供する関数を用いてモデルの更新をしたりHTMLを作ればよいだけであり、画面モジュール内部の実装詳細を知る必要はない。以上の理由から、画面単位のモジュールが作られる。

このような単位でモジュールを作ることで、「機能による区分」では過剰に公開されていたModelやUpdateの詳細が外部から適切にカプセル化され、意図しない依存が生まれにくい状態になっている。

余談: 共通化によって生まれる孤児モジュール

プログラミングをしている人間であればおなじみのDRY(共通化)は一般的には「機能による区分」で行われる事が多く、上記の「ドメインによる区分」の軸とはモジュールの設計の観点で直行することになる。

例えば「すべての画面モジュールにおいて"View"や"Update"の軸で共通化した実装を用意したい」などのケースがこれに当たる。

このような区分軸の直行が起こるケースでは、各ドメインを横断して仕様の分析を行い、その共通化が単なる「コード上の共通化なのかそれとも「概念としての抽象化」なのかを見極める必要が出てくる。

しかしながら、実際の事実としてもっとも起きがちなのは、このようなタイミングでcommonやutil, helperなどの「どこにも属さない概念」爆誕することである。彼らはコードとしては共通化されたが、概念上の所属の観点からは考えることを放棄された孤児モジュールである。

彼ら孤児モジュールは、ある意味「所属待ち」を行っている状態でもあるため、継続的にリファクタリングなどのタイミングでどこかのドメインに所属させたり、新しい抽象概念に昇格させたりしていくことが必要である。孤児モジュールがコードベースにおけるマジョリティーになってしまうと、アプリケーションにおけるドメインの概念が揺らいでしまう。

rbenvの初期化が遅いのをどうにかする

rbenvの初期化がすげー遅いときがある

eval "$(rbenv init -)"

探してみると、ちょっと昔の記事だけどこんなのが出てきた。

mattgreensmith.net

つまりrehashとかいうのが遅いらしいので、それをbackgroundで処理させるといい感じになるらしい。こんな感じで。

eval "$(rbenv init --no-rehash -)"
(rbenv rehash &) 2> /dev/null

たしかに体感早くなったような気がする。

DEVでフォームにおけるOpaque Typeの設計に関するシリーズを書いた

DEVにシリーズ機能というものがあることを知って試してみた。全2作。

dev.to

dev.to

elmbitsでも取り上げられた

内容について

全部は書けないのでざっくり。

一般的にOpaque Typeは以下のようなnew関数を公開して、バリデーションの結果Result型ないしMaybe型でカプセル化された型を返すような設計になっていることが多い。

type Name =
    Name String


new : String -> Maybe Name
new value =
    if String.length value > 100 then
        Nothing
    else
        Just (Name value)

ElmのパッケージではUrlモジュールなどがいい例で、fromString関数で受け取る文字列が正しいURLであればUrl型のデータがもらえる、みたいな設計になっている。

これはつまり、バリデーションを通らないものはアプリケーションのライフサイクル上存在できない(DDDで言うところの不変条件に近い)ものにするための設計手法だと言える。つまりJustであるものは常に有効なデータだと型で表現されている。

フロントエンドにおけるバリデーションと有効性

この考え方をフロントエンドに適用しようとすると少し難しい。なぜなら、フロントエンドは状態として「不完全」なものも受け入れなければならないことが多いからである。

ユーザーがフォームに文字列を入力している過程などは、必ず不正な値の状態から始まる。もしURLの入力を求めるフォームであれば、ユーザーがURLを入力し終えるまでは常にフォームに入っているデータの状態は不正であると言える。

これを素直にサーバーサイドと同じものとして捉えてしまうと、ユーザーの入力中のデータは不変条件を満たしていないので常に無効にする、のようなちぐはぐなロジックを生むことになる。フロントエンドでは入力中のデータも状態の一部として捉えるため、無効なデータであるとして破棄してしまうと永遠にユーザーが入力を完了することのできないフォームを作ることになってしまう。

つまり、フロントエンドではバリデーションにおける「有効」「無効」の状態以外に「入力中」のような状態が必要になる。では、これを現実的にどう設計し実装するか。これが、自分がDEVに投稿した"Designing Opaque Type for form fields in Elm"シリーズのPart 1でカバーしている内容である。

モジュール・コンポジション

Part 2ではPart 1で実装されたフォーム・モジュールの再利用性を高めるために、モジュール・コンポジションを用いた設計の導入を解説している。

f:id:IzumiSy:20200506145449p:plain
モジュール・コンポジション

Elmにはクラスの概念がないため、もちろん継承の概念もない。したがって、異なるモジュールの振る舞いを再利用するためには、特化したモジュールが抽象化したモジュールに対して処理の移譲を行うようにする。このような明示的な振る舞いの獲得を、一般的にはモジュール・コンポジションと呼ぶことが多い。

Elmにおけるモジュール分割は膾炙したノウハウがないためとっかかりが難しいが、この"Designing Opaque Type for form fields in Elm"のシリーズでは

  1. まず型に関する振る舞いを中心にモジュールを作成
  2. 同様の機能をもつモジュールから抽象的な振る舞いをさらにモジュールへ抽出

というふたつの流れに沿ってモジュール分割を行う手順を示している。

型の定義を中心としてモジュールをつくるという設計そのものはElmにおいて一般的ではあるが、さらにそこからどうモジュールを分けていくべきか、という点をモジュール・コンポジションの考え方と共に解説している。

リモワに向けてAmazonで買ったものまとめ

コロナによるリモートワークが始まるタイミングで、ついでにデスクまわりを充実させようと思っていろいろ買った。

iiyamaの19.5インチモニタ

自分はメインでiMacを使って作業をしているが、常に横にUbuntuマシンを立ち上げている。とくにUbuntuマシンである意味はないが、iMacを買ってマシンが余っていたというそれだけの理由。

27インチとか21インチだと、正直デカすぎていやなので、19.5インチでかつHDなこのiiyamaのディスプレイを選んだ。5年以上まえのディスプレイではあるが、サブとしてはかなりいい。

HUANUO モニターアーム ガススプリング式

上のiiyamaのモニタを固定するのに買った。

どうでもいいがAmazonでモニターアームを調べると、結局よく分からんメーカのジェネリック製品が多すぎてどれを選べばいいのか分からないので本当に困る。

自分の場合はもうめんどくさくなったので適当にこのHUANUOとかいうのを選んだ。いまんとこ壊れたりしてないので満足。

UGREEN 切替器 3.0 高速転送 USB 切り替え PC2台用

iMacUbuntuを同時併用するとなると、どうしてもマウスとキーボードが2ペア必要になる。これはかなりダルいので、USBの切り替え機を買うことにした。

レビューを見るに切り替えのラグがあると書かれているが、たしかにUbuntuと比べてiMacは体感3,4倍くらい遅い。Ubuntuのほうは早いので、これはおそらく切り替え機ではなくiMac側に問題があると思う。

サンワダイレクト キーボード&マウスセット

上の切り替え機を使うとなるとMagic Keyboardは使えなくなるので、同時にサンワダイレクトのキーボードを購入。できるだけMagic Keyboardに近いやつを探した結果、これになった。

ややチープ感はあるが、もともと自分はそんなにキーボードにこだわりのある人間ではないのですぐ馴染んだ。マウスがちょっと特殊だが、慣れると使いやすいのでウケる。

Logicool Anywhere S2

上のサンワダイレクトの製品に付属してくるマウスはチープすぎるのでこちらも購入。若干重さがある感じがよい。

サントリー ZONe

在宅ワークといえばエナジードリンク。ということで前から気になっていたこのZONeも買ってみた。

自分が買ったときはバージョン0.8.5だったが、この度メジャーバージョンアップして1.0.0になったっぽい。味はモンスターに近いが、もう少し甘みが強い。成分はあんまり期待しないほうがいい。

Satechi アルミニウム Type-C クランプハブ

まじでMac関連製品ってコレに限らずなんでこんな高いの?っていう感じだが、それでもこれはほしかったのでついでに買った。たしか誰かのNOTEで読んで存在を知った気がする。

iMacだとディスプレイの側面に内蔵されているスピーカーの側溝にはめ込む形でつけるが、これがあるとUSBに圧倒的にアクセスしやすくなる。

まとめ

こう見るとUbuntuマシンとの併用環境を整えただけみたいに見えるな。

でもUbuntuマシンはWindowsとのデュアルブートになっていたので、iMacを買ってからやれていなかったPCゲームが再び遊べて満足だからいいや。

iHerbでよく買うもの

過去に書いたこの記事を思い出したので、久しぶりに同じような記事を書くことにした。

izumisy.work

前提として自分はそんなにガチなトレーニーでもなんでもなく、どちらかといえば自分の健康増進のために筋力トレーニングをしたりサプリメントを摂取したりしている。

なので、ここで紹介するものもPFCバランス*1を考慮して...とかそういうものではない。

プロテイン

自分は痩せ型の体型ではあるが、それでも基準値で言うと1日で50-60g程度のタンパク質は摂取しておかなければならない。トレーニングをしない日でもタンパク質を摂取するのは重要である。

ALLMAXのクラシックは、1スクープあたり30gのタンパク質が含まれているので、これを朝に毎日飲むようにしている。これだけで一応必要なタンパク質量の半分は摂取することができる。

値段は約2倍になるが、以下のアイソフレックスのほうがピュアなタンパク質の粉末でありかつ味も本当にビターチョコという感じでよかった。

単に味がいいという理由でアイソフレックスのほうをしばらく飲んでいた期間があったが、自分の場合は食が偏りがちということもあり決してタンパク質だけがあればよいというわけではなかったため、いまはクラシックの方を飲んでいる。タンパク質以外の摂取量をコントロールしている人は、おそらくアイソフレックスを飲むのだと思う。

ALLMAX以前はマッスルファームのコンバットプロテインなども試したことがあったが、味が強すぎて毎日常飲するにはちょっとしんどいなという感じがあった。その点でいうと、まだALLMAXのほうが味が控えめだとは思う。

フィッシュオイル

心臓病のリスクを下げたりだとか、脳に良いとかそういうことがよく言われるフィッシュオイル。普段積極的に魚を食べるタイミングがないので飲んでいる。

医学的には魚を食べずにこのようなフィッシュオイルのサプリメントを飲むことにはあまり意味がないというような話も聞くが、一応プラセボ的な感じで信じている。

グルタミン

あのVASILY金山さんもおすすめ(?)のグルタミン。あの人が飲んでるのはグリコのやつだけど。

プロテインが攻めだとしたら、グルタミンはどちらかといえば守り、みたいなのをどこぞの記事で読んだ記憶がある。

筋肉トレーニングをしたあとの筋肉の分解を防ぐだとか、腸内の繊毛の働きを助けるだとかいろんなことを聞くので飲んでいる。大体の場合、なんか風邪ひきそうだな〜みたいなタイミングでちょっと多めに飲んだりする。

テアニン

頻繁に売り切れ状態になるので、在庫があるタイミングで2-3個一気に買っておくのがよい。

とはいえ、このテアニンとやら効いているのか効いていないのか謎なので、最近はあんまり買っていない。スマドラの記事で紹介したDMAEとかTrue Focusのほうがまだよっぽどキマる感じがある。


[PR] 自分の紹介コードはATI0904なので、もしiHerbが気になったら使ってみてください。

*1:タンパク質(protein)、糖質(carbohydrate)、脂質(fat)の三種類の栄養素の頭文字をとったもの

ZIOによるDIの最小サンプル

公式のドキュメントや他の人の書いたブログ記事を読みつつ書いてみたのでメモ。

zio.dev

import zio.{IO, RIO, Runtime, Task, UIO, URIO, ZIO}

// Userリポジトリのインタフェース

case class User(name: String)

object UserRepo {
  trait Service {
    def load(): Task[Option[User]]
    def store(user: User): Task[Unit]
  }

  def load: RIO[UserRepo.Service, Option[User]] =
    ZIO.accessM { env => env.load() }

  def store(user: User): RIO[UserRepo.Service, Unit] =
    ZIO.accessM { env => env.store(user) }
}

// ロガーのインタフェース

object Logger {
  trait Service {
    def info(msg: String): UIO[Unit]
    def error(msg: String): UIO[Unit]
  }

  def info(msg: String): URIO[Logger.Service, Unit] =
    ZIO.accessM { env => env.info(msg) }

  def error(msg: String): URIO[Logger.Service, Unit] =
    ZIO.accessM { env => env.error(msg) }
}

以上のインタフェースの実装を踏まえて、実装を用意する。

各実装は、上で完成したインターフェイスオブジェクト内部のServiceトレイトを実装している。

// オンメモリなUserリポジトリの実装(ただのモックだけど)

trait MemoryUserRepo extends UserRepo.Service {
  override def load(): Task[Option[User]] =
    IO.succeed(Some(User("seiya")))

  override def store(user: User): Task[Unit] =
    IO.succeed(())
}

// コンソール用ロガーの実装

trait ConsoleLogger extends Logger.Service {
  override def info(msg: String): UIO[Unit] =
    IO.succeed(println(s"[INFO] $msg"))

  override def error(msg: String): UIO[Unit] =
    IO.succeed(println(s"[ERROR] $msg"))
}

最後に依存する実装をミクスインしたDepsをアプリケーションに与えて起動する

object Deps extends MemoryUserRepo with ConsoleLogger

object ZIOApp extends App {
  val usecase =
    for {
      _ <- Logger.info("start")
      userM <- UserRepo.load
      _ <- userM.fold(Logger.error("no user"))(
        user => Logger.info(s"name: ${user.name}")
      )
      _ <- Logger.info("done")
    } yield ()

  Runtime.default.unsafeRun(usecase.provide(Deps))
}

2020年2月以降、ZIOには実装の依存関係を縦(Vertical)と横(Horizontal)の軸で合成できるようにするZLayerという機能が実装されたらしい。

自分も手元でZLayerを用いた実装を試してみようと思ったが、Scala力が足りないせいかコンパイルエラーが解消できなくて諦めた。

なので、このサンプル自体は動きはするがZIOのチカラが100%活かせているというわけではない。

実装と処理の分離で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)が分離されていることで、差し替え可能なコードになっていることが分かる。