Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

社会人4年目が終わった

先週で社会人4年目が終わり,今週から社会人5年目になりました.3年目で新卒入社した金融の会社を辞め,今は2社目に1年半弱在籍しています.振り返りをしておきたいと思います.

ちなみに私は社会人になってからプログラミングをはじめたので,プログラミング歴=社会人歴です.

できるようになったこと

テックリードになった

職位はそうなりました.前々からやりたいと思っていたのでよかったと思います.

うちのチームは,私とは別にエンジニアリングマネージャー的な人がいます.面談や全体の開発方針などはその人が決める構図です.なので,私は技術導入の責任と,製品の品質に対する品質,障害時の説明責任を負っています.

1からアプリケーションを作ってリリースできるようになった

アーキテクチャを1からデザインして,なおかつそれを実装する業務に関してはこれまでもやってきました.なのでとくに伸びてはいません.

一方で,実際に AWS に EC2 を立てて本番リリースする部分や,CircleCI を設定して CI/CD できるようにするといった部分は,3年目まではほとんどやってこなかったのであまりできませんでした.

4年目ではチャンスに恵まれ,CI/CD 周りの仕事をできました.今年習得したスキルの中でもっとも大きかったです.

関数型プログラミングに明るくなった

社内で Scala の cats 勉強会が開かれたのが大きかったです.cats には関数型プログラミングの説明兼 cats の使い方ガイドみたいな本があるのですが,その本を通して関数型プログラミングの手法を用いたアプリケーションのデザインを磨けました.

私は Java 出身でしたので,どうしてもデザインが Java によってしまっていました.それは Scala を使う上では決して悪いわけではありません.しかし,関数型プログラミングを勉強していくうちに,そこによく登場する手法を用いると,より安全で可読性の高いコードを書けると気づきました.ただ,実際のアプリケーションにどう適用したらよいのか,いまいちイメージがついていませんでした.

cats の本を通して,その本に掲載されているふんだんな実用例をもとに,おおよそデザインに関するエッセンスを掴めました.

Pros/Cons を考えながら技術選定できるようになった

今まではあまり深く考えずに好きなものを好きなだけ使うor自前で作ってしまえ,といったスタンスだったのですが,だいぶ技術選定の際に理性を働かせられるようになりました (笑).

技術選定にはいくつか基準があると思いますが,たとえば:

  • チームのスキルセットを鑑みて,ここまでのレベルは扱えるだろうと予想して技術を使う
  • あるいは,チームのスキルセットとあるべき姿を照らし合わせて,ここのスキルセットはチームメンバーに欲しいからあえてこの技術を使おう/やめておこう
  • ライブラリのリリースノートを見て,どの単位でリリースしているのかや,そもそもちゃんと説明責任を果たす作者なのかを確認して,このライブラリを使おう/やめておこう
  • Issue/PR に対する作者の対応を見て,このライブラリを使おう/やめておこう
  • そのプログラミング言語の日本におけるコミュニティの対応状況や盛り上がりを見て,使おう/やめておこう
  • 今後5年くらいは,こういう流れになっているだろうから,この技術を使おう
  • 採用市場にこの技術を扱えるエンジニアが多い(あるいは,これから増えてくる)から,この技術を使おう

こういったことを考えながら,技術選定できるようになってきたかなと思います.もちろん,その技術を好きではあるものの,実際に使用するのは封印しなければならない技術もあります.Rust とか.我慢がきくようになったともいいますね (笑).

低レイヤーの話がかなりわかるようになった

実際に自分でいくつかコンパイラを実装しました.また,会社のゼミ中にネットワーク周りの調査や実装を行いました.それによって,低レイヤーの知識に実感が伴うようになりました.

知識にはいくつかの習熟段階があります.まず,本を読んで概念を理解すること.次に,実際に使ってみて概念を肌感で覚えること.最後に,自身の中で得た知識を体系的に整理し,人に教えること.

これまで低レイヤーの知識に関しては,本を何冊か読んで「概念的には知っている」状態の知識がほとんどでした.しかし,いくつか手を動かしたことによって実感を伴いました.引き続きやっていきです.

Go 言語を書けるようになった

これはちゃんとアプリケーションのリリースまで含めてやったので,書けるといっていいはずです (笑).

最近,弊社にインターンに来る子や新卒の子が,ハッカソンインターンシップ中に Go 言語を多く使うようになってきました.中には8割の参加者が Go 言語を使用したインターンシップもありました.

当時,私自身はあまり Go 言語に関心がなく (というのも,Go 言語の代替で使える言語をいくらか使えるので),今後も触る予定がなかったのです.しかし Go 言語をあまり触っていないがために,インターンシップの評価者として入ったとき,的確な評価を下せず歯がゆさを感じる場面がありました.なので時間を取って勉強してみました.1週間くらいでかなり書けるようになりました.

その後,新規事業で Go 言語を使う機会が訪れました.プロダクションでも無事 Go 言語を使えました.

ただ,普段は使わないので最新情報のキャッチアップが課題ですね.もっとも,優先度は高くないのでタイムラインで見たものをチラッと見る程度で十分だとは思いますが.

Rust 関係での登壇

いくつかしました.Rust は触り始めてからもう2年近く経ちますね.昨今は国内・海外ともに採用事例が増えてきている上に,Reddit を見ていると Rust はよくバズっているので,注目度の高まりを肌で感じられるようになってきましたね.

コンテナのうれしさがわかるようになった

いろいろ苦労しましたがやっと.これまで,私の所属しているプロダクトにはコンテナがほとんど採用されていませんでしたが,いくつか機会があって導入しました.

やろうと思っていたけどなかなか手が伸びなかったもの

Linux そのものに関する話

アプリケーションエンジニアとしての時間が長いので,普段から macOS x IDE で大半の作業を行います.なので用意されているコマンド,OS の仕組み等々込で Linux が結構苦手でした.

前職は Windows だったため,転職当初はほとんど Linux をよくわかっておらず,障害時などにとくに困っていたのですが,1年半経ってある程度操作には慣れたように思います.しかし,まだ周りのエンジニアを見ているとその平均的な水準には届いていないかなと思っています.

が,あまり関心が向かないのか,ほとんど手をつけられませんでした.

アルゴリズム

概念的には理解できるけど,いざ実装する・使いこなして問題を解くとなるとちょっと苦手ですね….慣れていないだけだと思います.

これも去年何冊か本を買っては見たものの,結局関心が向かないのかなんなのかで,あまり目を通せませんでした.

ただアルゴリズムはやっておかないとこの先外資系に行きたい場合などに苦労することになりそうなので,まとまった時間をとって勉強しておきたい.TopCoder をやるなどして強制的にやらないといけない環境に置いたほうがよいかもしれません.

AWS の認証,ネットワーク周り

何度か体系的に知識を得て整理しようと思っていたのですが,結局関心が向かないのかなんなのかでやっていません.が,ついに今週体系的な知識の必要性を感じる場面があったので,これに関してはようやくモチベーションが湧いてきました.やります.

データ分析・SQL

やれば楽しいに決まっているし,実際チャンスもたくさんあるチームなのでやろうと思えばやれる!と1年くらい思っていましたが,食指が動きませんでした.毎年新卒のデータアナリストが入ってくるたびに簡単な練習問題を出しているので,今年は私もそれに参加させてもらおうかなと思っています.

あと SQL はいまだにあまり伸びてない.ORM 畑育ちなので,生の SQL はまだ抵抗がありますね.これも,新卒のデータアナリストと一緒に勉強しようかなと思います.

5年目で手を伸ばそうと思っているもの

仕事面

理想像はいるの?

これ,私と一緒に働いている方は驚くかもしれませんが,同じ部署内にいます.その人と比べて自分に足りないところはおおよそ次の通りかなと思っているので,何年かかけてのんびり力を伸ばせていけたらいいなと思っています.

伸ばしたいスキル

  • Linux,シェル: 基礎教養だと思うので.
  • 分散処理関係の話にもう少し詳しくなりたい←漠然としているのはまだ何があるかわかっていないからです
  • AWS のネットワーク,認証周り: 概念の整理と理解,ならびにどこをどう編集したら自分のやりたいことが実現できるかをスムーズに手順立てできるところまで.
  • データを前処理して Python で可視化するところまでできるようになりたい.

プライベート

プログラミングやコンピュータそのものが好きだとこの4年目を通して気づいたので,それ関連をプライベートでも進めていきたいと思っています.

Functor,Applicative Functor について勉強したので整理してみる

最近 Functional Programming in Scala の勉強会をずっとしているのですが,ようやく12章の Applicative Functor に入りました.ところが,急に登場した Applicative という概念がいまいち勉強会の時点ではつかめておらず,少し頭の中を整理したいと思ったので簡単にまとめてみようと思います.あまり体系的にまとめるつもりはありません.自身の理解のメモとしてインターネットに放流しておく予定です.

(Applicative Functor はアプリカティブファンクタと以下書きます.日本語ググラビリティのためにそうします.他のこれ関連の用語も同様にカタカナで表記します.)

なお,モノイドやモナドは登場させません.これには意図があって,『すごい Haskell たのしく学ぼう!』という本では,モナドの説明なしにアプリカティブファンクタの説明をじっくり行っていたためです.この説明はわかりやすいなと思いました.今回はこの本の11章を参照しながらいろいろと考えていきます.

勉強会で使用している本はこちら.

すごいHaskellたのしく学ぼう.これは関数型プログラミングの理解の助けになる本だなと思います.

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

コードそのものは Scala で書きます.すごい Haskell 本のソースコードを読みつつ,Scala で書くとこうなるかなと翻訳して書いています.ただ厳密にはちょっと違いそう.

ファンクタとは

ファンクタとは「関数で写せるもの」のことを言います.要するに写像 (map) です.といったところで,???が頭に浮かぶかもしれません.さっそくですが実装を見てみましょう.

trait Functor[F[_]] {
  def fmap[A, B](fa: F[A], f: A => B): F[B]
}

これを見ると一目瞭然なのですが,F の中身の型を A から B に変えているものですね.F にはたとえば OptionList といった型が入ります.この外側の型を保ちながら,内側の型を別の型に変換することをしてくれる抽象的な概念をファンクタと呼びます.難しくいうと,値があるかないかという Option の文脈を保ちながら,型を変換しているということです.

ファンクタは1引数しか受け取ることができません.なので,Either のような2つの型変数をもつ型をファンクタにするには,一度部分適用をして,あと1つ型変数を引数に取るだけの状態にする必要があります.

さて,ファンクタのざっくりとした説明はここまでです.11章では lift などの概念も実は登場してきていますが,今回は省きました.

アプリカティブファンクタとは

さて,欲しくなる場面ですが,たとえば次のようなコードがあったとしてみてください.

val f = n => n * 3
val someF = Some(f)
val some5 = Some(5)
// いい感じに Some(f(5)) をしたい

Some(f) の f に Some(5) の5を適用したいとなった場合にどうしたらよいのでしょうか.普通のファンクタを扱う限り,これはちょっと難しそうです.というのも,普通のファンクタでできるのは,「通常の関数で」「ファンクタの中の値を」写すことだけだからです.上述したように,一度 Some の中の値を両方とも取り出して,再度 Some に詰め直すという作業をするか,あるいは fmap を実装しているのであればそれを2回適用すればよさそうに見えます.

ただ,もう少しスマートにやる方法があります.それがアプリカティブファンクタという概念です.コードを見てみましょう.

trait Applicative[F[_]] extends Functor[F] {
  def pure[A](a: => A): F[A]
  def <*>[A, B](fa: => F[A], f: F[A => B]): F[B]
}

要するに,Fの中に入った関数を元の F に適用 (apply) して,その結果値を受け取ることができるものです.それゆえに applicative …?ただ,こうすることで,ファンクタでは1引数関数でしか処理できなかったものを,アプリカティブファンクタでは2引数関数で処理できるようになります.これはファンクタよりも強力になったことを意味します.

pure というのは,なんでもない a という値を受け取ると,それを F の中に入れて返すものです.<*> は,見たとおりで F という型の中で関数を適用してくれるスグレモノです.

ところで,Haskell の Applicative にはさらに便利な関数があるようです.それが,liftA2 という関数です.liftA2 は,「通常の2引数関数を,2つのアプリカティブ値を引数に取る関数に昇格させる」という機能を持っています.実装を見てみましょう.

trait Applicative[F[_]] extends Functor[F] {
  def pure[A](a: => A): F[A]
  def <*>[A, B](fa: => F[A], f: F[A => B]): F[B]
  def liftA2[A, B, C](a: F[A], b: F[B], f: (A, B) => C): F[C] // ←追加された
}

実装のとおりです.ここまでそろうと,ようやくアプリカティブファンクタを実装することができます.実装してみました.

trait Applicative[F[_]] extends Functor[F] {

  def pure[A](a: => A): F[A]

  override def fmap[A, B](fa: F[A], f: A => B): F[B] = this(fa, pure(f))

  def <*>[A, B](fa: => F[A], f: F[A => B]): F[B] =
    liftA2(f, fa, (_: A => B)(_: A))

  private def apply[A, B](fa: => F[A], f: F[A => B]): F[B] = <*>(fa, f)

  def liftA2[A, B, C](a: F[A], b: F[B], f: (A, B) => C): F[C] =
    this(b, fmap(a, f.curried))
}

コンパイルも通ったしたぶん大丈夫…!private defapply を追加したのは,単純に <*> を使おうとするとうまいこと中置記法できなくてかっこ悪いと思っただけです.あんまり深い意図はありません.

ところで <*>fmapliftA2 が循環参照しているので,最終的にこの Applicative を使う側でどこかを実装してやる必要があるのでしょうか.Functional Programming in Scala の中でも循環参照している実装例が出てきたのですが,使う側で個別実装したら大丈夫だったので,たぶんそういうことなのだと思っています.

ということで,少し実装をしてみましょう.たとえば Maybe という型があったとします (Haskell から拝借).

sealed trait Maybe[+A] {
  def <*>[B](f: Maybe[A => B])(implicit F: Applicative[Maybe]): Maybe[B] = F <*> (this, f)
}

object Maybe {
  case class Just[+A](a: A) extends Maybe[A]
  case object Nothing extends Maybe[Nothing]
}

object applicatives {
  implicit def maybeApplicative: Applicative[Maybe] = new Applicative[Maybe] {
    override def pure[A](a: => A): Maybe[A] = Just(a)
    override def fmap[A, B](fa: Maybe[A], f: A => B): Maybe[B] = fa match {
      case Just(a) => pure(f(a))
      case Nothing => Nothing
    }
    override def <*>[A, B](fa: => Maybe[A], f: Maybe[A => B]): Maybe[B] =
      f match {
        case Just(something) => fmap(fa, something)
        case Nothing         => Nothing
      }
  }
}

これであっているのかは若干怪しいところですが (ちょっと美しくない実装な気もする),おおよそのイメージは掴んでもらえるはずです.実行して試してみましょう.

object Main extends App {
  val just5 = Just(5)
  val justF = Just((n: Int) => n * 3)
  implicit val maybeApplicative = implicitly[Applicative[Maybe]](applicatives.maybeApplicative)
  println(just5 <*> justF)
}

これを実行すると,結果は

> Just(15)

と返ってきます.やりたかったことが実現できましたね.

まとめ

  • ファンクタは map のことであり,関数の写しである.F の中身を単に取り出して変換する.
  • アプリカティブファンクタは Haskell だと <*> であり,2つの F を受け取って,両者を引数に受け取る関数を適用して結果値を取り出したり,あるいは,F の中に入った関数を F の中で適用して結果値を受け取ることができる.
    • まとまながらなるほどなと思ったんですけど,これがゆえに Functional Programming in Scala では Traverse[F[_]] の説明に入ってゆくのですね.

しかし,とくにアプリカティブファンクタの方がまだまだ理解が100%になった気はしませんね.つかめるまでもう少しエクササイズをしたいと思います.

Run Length Encoding

『問題解決のPythonプログラミング』という本を読んでいたら出てきたエクササイズです.おもしろそうだったのでやってみました.もともとその章のお題だった配列をぶん回すという方針に従って,それを応用して今回はやってみました.各文字に対してカウンタをもつ HashMap なり Dictionary をもたせるとよりシンプルになりそうな気がします(Python での Dictionary の書き方がまだ習得できておらず面倒でやってない).

Disclaimer

Python 素人なのでもしかすると罠を踏んでいるかもしれません.

Run Length Encoding とは

たとえば次のような文字列があったとします.

BWWWWWBWWWW

この文字列を Run Length Encoding をもちいて圧縮すると次の文字列に圧縮されます.

1B5W1B4W

文字を1つ1つ分解して,重複して出てきた回数を文字の左に表示することで表現しています.当初は 11 byte (使用する言語により異なりそうですが,今回は便宜上1文字=1 byteと計算します) だった文字列の確保領域が 8 byte に減少しています.

実装してみる

さて,これを Python で実装してみました.

def run_length_encoding(strings) -> str:
    caps = list(strings)

    if len(caps) == 0:
        print('empty list')
        return ''

    caps = caps + ['#EOF']
    counter = 1
    encoded = ''

    for i in range(1, len(caps)):
        if caps[i] == caps[i-1]:
            counter += 1
        else:
            print(counter, caps[i-1], sep='', end='')
            encoded = encoded + str(counter) + caps[i-1]
            counter = 1  # counter reset

    return encoded

ポイントは #EOF という文字列を最後に追加しているところですね .caps という配列をインデックスで回して,前後の文字列が一致している限りはカウンターを動かすということをしています.異なった瞬間にプリントアウトしつつカウンターを初期値に戻しています.文字列が異なることを reduce のトリガーとするため,最後の #EOF を入れないと,最後の方の文字列のカウントがうまいこといきません.[*1]

さて,問題には decode もしろと書いてあったので decode もしましょう.1B5W1B4W という文字列を元に戻していきます.

def run_length_decoding(strings) -> str:
    caps = list(strings)

    if len(caps) == 0:
        print('empty list')
        return ''

    decoded = ''

    for i in range(1, len(caps)):
        if caps[i].isalpha():
            char = caps[i]
            count = caps[i-1]
            decoded = decoded + char * int(count)

    return decoded

また同様に,まずは文字列を分解して配列に直します.配列をインデックスで回します.アルファベットの箇所に到達したら,今回のエンコード方式ならばすぐ左に数字があるはずなので,その数字を取り出して回数分文字列を生成します.Python だと,文字列 * int で int の数値分文字列を生成できます.これを活用していきましょう.

アルゴリズムのカタログというよりは,目の前の問題をプロはどのように解いていくのか?という部分の解説に主眼を置いたいい本だなと思います.MIT の授業かなにかが書籍化したっぽいですね.私は CS の教育は受けていないので,こういう本が非常に助かります.Python なのも嬉しい.Python ならわかる.

*1:この文字列,非常に危なっかしいですね.別のトークンを用意してあげる必要はありそうですが,まあ今回は本書の手法に従いたいのでそうしました.Map を使用すればこのようなことは必要ありません.

Servo の開発に出てくる highfive について

小ネタもうひとつ.

Servo の開発をするとお世話になるのが highfive という bot です.新規コントリビュータの人が参加しやすいように作られた bot だそうです.アイコンかわいい.

github.com

お世話になる場面は下記の2つです.

  • Issue を自身に割り当てる
  • PR のラベル管理

Issue を自身に割り当てる

Servo はまだまだ新機能の開発ややり残したタスクなどが Issue としてたくさん上がっています.これはすべてのデベロッパーに開放されており,やろうと思えばいつでも Issue に取り組むことができます.その最初のタッチポイントとして highfive が登場します.

自身が担当したい Issue に次のようなコメントを残すと,自身にその Issue がアサインされます.

@highfive assign me

するとこんな感じで,Issue にラベルがつけられて自身に Issue がアサインされます.

f:id:yuk1tyd:20190303235858p:plain

f:id:yuk1tyd:20190303235912p:plain

PR のラベルの管理

Servo では,S-awaiting-review (レビュー待ち) といったラベルで PR の状況を管理しています.ラベル管理はほぼすべて highfive が行っていますね.できる子…!

余談ですが,最初に PR を投げるとまず自動的に S-awaiting-review というラベルが付与されます.その後,もしコードの修正が必要なようであれば,S-needs-code-changes というラベルが付与されます.さらにテストが落ちていたりすると,highfive が S-test-failed というラベルが付与されます.落ちたテストが修正コミットにより再び通ると highfive が S-awaiting-review を付与し,S-test-failed のラベルを取り除きます.

このあたり,人力でやると運用がカオスになってしまいがちなので,こういった bot に自動化させておくというのは非常に正しいなと思いました.

highfive 自身

ちなみに Highfive はソースコードが公開されています.

github.com

上小ネタでした.

Servo の開発で出てくる bors-servo について

Mozilla 関係のプロダクトにコントリビュートするとよく見かける (?) ,bors というボットがいます.たとえば,普段コントリビュートするみなさんもこういったコメントを見たことがあるかと思います.

@bors-servo r+

普段は自分でキックすることはないので,基本レビュワーの方々に任せておけばいいかな〜というスタンス(そしてコケたら対応すればいいやのスタンス)なのですが,せっかくの機会なのでいろいろ調べてみることにしました.

そもそも bors とは

裏側は Homu というツールが動いています.Homu とは,リポジトリのコードのすべてのテストがいつも通っている状態を自動的にキープしてくれるツールのことです.自動テストツールといったところでしょうか.

個人の方が作っていた OSS です.ただ作者の方はもう GitHub 上ではアクティブではないため,Servo が fork してメンテナンスして使っているようです.

github.com

github.com

公式ガイドによると次の手順で動作するようです.

  1. 開発者の working branch はいつもどおりアップロードされる (要するに fork なりした branch から普通に commit & push するということですかね).
  2. レビュワーがコードをチェックして大丈夫そうならば,Homu に対してメッセージを送る (Servo なら @bors-servo r+ などというコメントを見ますね).
  3. Homu は master ブランチにマージを行う (が,それは本当の master とは別になっていて auto と呼ばれるもの).
  4. サーバーのクラスタ内で,すべての OS に対するテストが行われる.
  5. クラスタが OK を返すと,Homu は auto を master にコピーしたと返答する.
  6. クラスタが NG を返すと,Homu はそのエラーレポートを返す.何もしない.

コマンドのチートシート

詳しくはこのガイドに載っています.

build.servo.org

よく見るものだけ軽くまとめておきます.

  • r+: PR の承認を意味します.これが出るとだいたい OK 感ある.bors の種々のテストが行われた後,マージされてその PR は Close となります.
  • try=xxx: 承認なしでテストだけ走らせることができます. PR を投げた直後に飛ばしていました.try → テストが OK → r+ の流れな気がします.
  • retry: 文字通り retry します.普通にテストが別のツールで通っているのに落ちることがあり,変だなと思ったら投げているようですね.

Issue は自動的に閉じられる

担当した Issue は,bors-servo が最後に自動的に閉じて後片付けまでしてくれます.これなら閉じ忘れも起きませんね.

Servo の開発においては,あらゆることが自動化されていて,OSS とはこういう感じなんだなあという勉強になることが多いです.

小ネタでした.

ポケモンのタイトルになっているプログラミング言語を調べてみた

暇ではないです.言い出しっぺの人晒しときます.

歴代のタイトル

面倒なので,まずは日本語のタイトルを都合よく解釈して英語にしていきます.

ソードシールドは今年発売みたいですね.

調べ方

調べ方: 「XXX Programming Language」で検索するとだいたい出てくるのでそれで.Googleで出てこないプログラミング言語は知りません.あと,個人が GitHub 上に実験目的で公開しているものは割愛します.教育機関や企業が作っているもの,あるいはかなり有名になった言語を紹介したいなと思います.

結果

急いで調べて資料に目を通したので,間違っている箇所があるかもしれません.間違いを見つけられた際はご指摘ください.あと,「この言語忘れてる!」もお願いします🙂 部分一致でもいいです!

感想

  • 疲れた.
  • 結構あるな.
  • もうちょっと深掘りして後日書き足しますね.一旦これで.

追記

抜けてる奴の中だとウルトラサンムーンとポケモンダッシュはありそう

ウルトラサン/ウルトラムーンは,「Ultra」と「Sun」に関しては検索してもありませんでした…ポケモンダッシュは完全に忘れてたので,Dash で検索すると次の言語が出てきました笑.

blog.eqrion.net

2015年くらいに個人の方が作られた言語のようですね.すべて C で実装されているようです.

IO モナドを使った Web アプリケーションの構築

Scala の記事を書くのは地味に初めてかもしれません.今回は,Scala の cats-effect というライブラリの中にある IO モナドを使って Web アプリケーションを構築してみたいと思ってやってみたので,その話を簡単にメモしておきます.

cats とは?

Scala 界隈だとかなり有名です.Scalaz か cats を選ぶことが多いと思います.Scalaz も大変すばらしいライブラリですが,今回は cats の話を少しします.

cats は関数型プログラミングScala で行うために必要な抽象度の高い関数セットを提供するライブラリです.抽象度の高い関数セットとはどういうことか…私なりの理解で行くと,通常 ScalaList[A]Option[A] などには flatMap という関数がついていますよね.あれはあくまで,個別具体のモナドに対する個別実装がなされています.一方で,flatMap という命令は,実は型にすることで抽象化が可能です.具体的には trait FlatMap[F[_]] とすることで可能です.

F[_] というのは Scala では型構築子と呼ばれ,この中にはたとえば OptionFutureList などの,F[_] となりうる条件を満たす型を入れることができます.つまり,FlatMap[F[_]] は,Option などにミックスインしていくことによって,flatMap ,命令を付け足していくことができる.FlatMap[F[_]] として抽象化されているので,自身で独自のモナドを作ることもできます.そのための道具を提供しているライブラリである,ということです.

また,これらの抽象的なパーツを存分に組み合わせたモナド変換子なども提供されており,非常に強力な関数型プログラミング向けのライブラリです.

typelevel.org

cats-effect の IO モナドとは?

IOFuture は,Future が即時評価なのに対して,IO は遅延評価を行うという点で異なります.遅延評価なのでスタックセーフですね.

もちろんモナドなので,Future と同じようにコンビネータでどんどんつなげていくことができます.詳しい機能は下記の公式ドキュメントがあるのでそちらもご覧ください.

typelevel.org

私が個人的に好きなのは,スレッドの切り替えを明示できる関数 contextShift をもっていることです.これを明示的に書くことができるのはわかりやすくていいですね.

なお,Future から IO への切り替えは IO#fromFuture という関数でできます.なので,Future を使っているライブラリの関数も IO に切り替えて使用することができます.

IO モナドの中身をちょっと見てみる

IO モナドの中身ですが,次のような代数的データ型をもっており,各々のケースクラスに応じて中身の評価が遅延で (Pure の場合は即時で) 走ります.

  private[effect] final case class Pure[+A](a: A)
    extends IO[A]
  private[effect] final case class Delay[+A](thunk: () => A)
    extends IO[A]
  private[effect] final case class RaiseError(e: Throwable)
    extends IO[Nothing]
  private[effect] final case class Suspend[+A](thunk: () => IO[A])
    extends IO[A]
  private[effect] final case class Bind[E, +A](source: IO[E], f: E => IO[A])
    extends IO[A]
  private[effect] final case class Async[+A](
    k: (IOConnection, Either[Throwable, A] => Unit) => Unit)
    extends IO[A]

IORunLoop#loop という関数の中で,各代数的データ型に対する処理が走ります.

IO モナドと Web アプリケーションをつなげるには - http4s

多くのライブラリでは,Future を使用しています.たとえば Akka-HTTP は scala.concurrent.Future を使用しています.

IO には IOApp という,IO ベースでランタイムを起動できるトレイトが用意されていて,それを使用すると綺麗に実装を書ききることができるんですね.

上の IO#fromFuture を使って Akka-HTTP と IO をつなげてみようと苦心したんですが,やり方がよくわからず諦めました.

そのかわりに今回は http4s というライブラリを使用することにします.このライブラリは,IO を使って処理をつなげていくことを前提として設計されており,cats フレンドリーを謳っているだけあって,cats と組み合わせて使うと非常に心地がよかったです.前置きが長くなりましたが実装していきましょう.

http4s | http4s

実装してみる

要件

シンプルに行きましょう.使い方を見てみたいだけなので.

  • とりあえずヘルスチェックを返すエンドポイントをもったアプリケーションを作る

構成

次のような構成を取ります.

  • Bootstrap: いわゆる main 関数.
  • EndpointProvider: エンドポイントの設定をまとめあげるトレイト
  • Endpoint: エンドポイントを表現するトレイト

リポジトリはこちらです.sbt の設定などはリポジトリをご覧ください.

Main 関数を作ってみる

StramApp[F[_]] という起動まわりの処理を一手に担ってくれるトレイトが提供されていますので,これを使って構築していきましょう.

なお,MixInEndpointProvider というのは Cake Pattern のボイラープレートです.Cake Pattern というのは,DI の手法のひとつです.DI ライブラリを使うとエラーメッセージがよくわからない上に実行時にしか DI に成功したか失敗したかがわからないことが多いですが,Cake Pattern はコンパイルタイムである程度うまく依存関係を解決できているかどうかわかるという点でいいなと思っています.今回は余計なライブラリを依存関係に含めたくないのもあって,Cake Pattern を使用しています.

import cats.effect.IO
import endpoints.MixInEndpointProvider
import fs2.StreamApp
import org.http4s.server.blaze._

import scala.concurrent.ExecutionContext.Implicits.global

object Bootstrap extends StreamApp[IO] with MixInEndpointProvider {
  override def stream(
      args: List[String],
      requestShutdown: IO[Unit]): fs2.Stream[IO, StreamApp.ExitCode] = {
    BlazeBuilder[IO]
      .bindHttp(8080, "localhost")
      .mountService(endpointProvider.endpoints)
      .serve
  }
}

エンドポイントの提供元を作る

テストをする際に楽にしようかなと思って,型構築子をつけて型クラスを生成するようにしましたが,正直あまり意味がないかもしれません.やってみたかっただけということで.MixInEndpointProvider[F[_]] としておきたかったし,きっとそういう手法もあると思うんですが,implicit の値を別で与える必要が出てきて解決策が見えなかったので妥協しました.

package endpoints

import cats.effect.IO
import endpoints.api.MixInHealthCheckEndpoint
import org.http4s.Method.GET
import org.http4s._
import org.http4s.dsl.impl.Root
import org.http4s.dsl.io.{->, /}
import org.http4s.server.middleware.CORS

trait EndpointProvider[F[_]] {
  def endpoints: HttpService[F]
}

object EndpointProvider {
  implicit val endpointProviderIO: EndpointProvider[IO] = new EndpointProvider[IO] with MixInHealthCheckEndpoint {
    override def endpoints: HttpService[IO] =  CORS(HttpService[IO] {
      case GET -> Root / "api" => healthCheckEndpoint.endpoint
    })
  }
}

trait UsesEndpointProvider {
  val endpointProvider: EndpointProvider[IO]
}

trait MixInEndpointProvider {
  val endpointProvider: EndpointProvider[IO] = implicitly[EndpointProvider[IO]]
}

さて,また別の MixIn がいますね.これが今回のヘルスチェック用エンドポイントの本体です.深掘りしていきましょう.

package endpoints.api

import cats.effect.IO
import org.http4s.{Response, Status}

trait HealthCheckEndpoint[F[_]] extends Endpoint[F]

object HealthCheckEndpoint {
  implicit def healthCheckEndpoint = new HealthCheckEndpoint[IO] {
    override def endpoint: IO[Response[IO]] = IO.pure(Response(Status.Ok))
  }
}

trait UsesHealthCheckEndpoint {
  val healthCheckEndpoint: HealthCheckEndpoint[IO]
}

trait MixInHealthCheckEndpoint {
  val healthCheckEndpoint: HealthCheckEndpoint[IO] =
    implicitly[HealthCheckEndpoint[IO]]
}

ヘルスチェックなので,単に 200 OK を返すだけです.

IO#apply によって IO を生成します.これは先述の Delay という case class を中で呼んでおり,この Delay が実質遅延評価になっています.ちなみに IO#pure を呼び出すと,即時評価の IO が生成されます.

IO#apply は同期処理が走ることに注意が必要です.非同期処理を走らせるには IO#async を呼び出す必要があります.明示的に書かせるという点でよい設計だと思います.

これで一応は動くようになりました.ただ,受け取った JSON をどうするかとか,あるいは JSON にして返したいパターンなどはまだ試していません.

感想

IO で非同期処理をコンビネータにしてつないで書いていくことができるというのは大きいですね.Scala には for-yield があるので,コールバック地獄もそれを使うことによって避けることができます.

IOFuture との変換もしっかり考えられているので,スタックが厳しい箇所に部分的に IO モナドを使うといった使い方もできそうですね.最近仕事でそういう箇所があり,ぜひ使ってみたいなと思いました.

http4s そのものは,とくだんハマりポイントもなく軽量ないいライブラリだと思います.

Cake Pattern の Uses, MixIn といったボイラープレート側を型構築子で持たせて,DI する瞬間に型クラスで中身が切り替わるみたいな実装をしたいのですが,それについてはまた調査をして記事にしようかなと思います.ご存知の方いらっしゃいましたら教えてください🙇