Don't Repeat Yourself

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

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 する瞬間に型クラスで中身が切り替わるみたいな実装をしたいのですが,それについてはまた調査をして記事にしようかなと思います.ご存知の方いらっしゃいましたら教えてください🙇

Procedural Macros に入門していたずらしてみた

この記事は Rust Advent Calendar 2018 25日目の記事です.

Rust 2018 edition より,Procedural Macros (以下もずっと英語表記します) という機能が新たに追加されました.

Procedural Macros においては, TokenStream というストリームが概念の中心にあります.TokenStream と呼ばれる抽象的なトークンのストリームを引数として受け取り,何かしらの加工をかけて TokenStream を返すという処理を行います.AST を受け取って,別の AST に加工して返すと説明したほうがわかりやすいもしれません.中にコンパイラがいるというイメージでしょうか.

本日の記事ですが,まず動かす段階でちょっと躓いたので簡単に動かし方を書いておきます.その途中でいろいろ遊んでみたので,その結果も書いておきます.内部実装がどうなっているかの話もちょっとだけ書きました.本当に入門してみただけの記事です.

なお,時間の関係で全容がよくわからなかったので,疑問に思ったことを記しておきます.あとで疑問を解消して記事にすると思います.知見をもっている方がいらっしゃいましたら,ぜひ教えてください🙇

とりあえず使ってみる

ドキュメントにある例を試してみました.が,結構ハマりました.GitHub 等で examples を探した際に,Rust 2015 のものしかまだ出てきませんでした.ただ,次のような手順でサンプルは実行可能でした:

  1. Cargo.toml に proc-macro 用の設定を追加する
  2. lib.rs にマクロを記述する
  3. main.rs で呼び出しをしてみる

実行環境は,Rust 1.31.1 の stable 版です.

順番に説明していきます.

1. Cargo.toml に proc-macro 用の設定を追加する

こんな感じで用意してみます.シンプルに [lib] を追加するだけです.

[package]
name = "hello_procedural_macros"
version = "0.1.0"
authors = ["XXX"]
edition = "2018"

[lib]
proc-macro = true

[dependencies]

2. lib.rs にマクロを記述する

extern crate proc_macroproc_macro 用のクレートを追加しておきます.現時点 (2018-12-25) では,CLion の Rust プラグインではこのクレートを認識してくれず,見つかりませんというエラーメッセージが表示されました.ただ rustc によるコンパイルは通ります.静的解析がうまくいってないだけみたいですね.

|> lib.rs

// クレートを読み込む
extern crate proc_macro;

// マクロを作成する用に TokenStream をインポートしておく
use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    "fn answer() -> u32 { 42 }".parse().unwrap()
}

TokenStreamFromStr を実装済みです"fn answer() -> u32 {42}" は &str 型で,parse() を利用して &strTokenStream への変換が可能 (parse 結果は Result 型なのでさらに unwrap が必要) になっていますね.

TokenStreamFromStr 実装を見てみると,中で __internal::token_stream_wrap() という関数が呼ばれていました.__internalTokenStream の内部用のモジュールとして定義されています.さらにざざっと中を見てみると,Procedural Macros 用の仮のソースファイルを生成して,生成したファイルを Lexer に投げているようでした.つまり,中で字句解析をして TokenStream に詰め直して返しています.その後のフェーズに関しては追いきれませんでした.評価フェーズとかどこに書いてあるんや.

解説記事を見る感じだと,Rust 本来の AST ノードとは別で,TokenStream というノードを独自に持つことによって, Rust 側の AST の内部実装に対する変更を吸収できるように設計した,みたいな感じの意図のようです.

3. main.rs で呼び出しをしてみる

Rust 2018 から,モジュール機能が改善されたため,従来であれば extern crate hello_procedural_macros という記述が main.rs の先頭に必要だったのですが,それがなくなりましたね.

それはさておき,main.rs に先ほどの make_answer マクロを呼び出していきましょう.下記のようになるかと思います.

make_answer というマクロの中にある answer() という関数を呼び出しています.

|> main.rs

use hello_procedural_macros::make_answer;

make_answer!();

fn main() {
    println!("{}", answer());
}

さて,このコードを実行してみましょう.実行してみると,

|> terminal

$ cargo run
   Compiling hello_procedural_macros v0.1.0 (/Users/xxxx/dev/rust/hello-procedural-macros)                                                                                                                                                                                                                                                                    
    Finished dev [unoptimized + debuginfo] target(s) in 1.30s                                                                                                                                                                                                                                                                                                   
     Running `target/debug/main`
42

期待通り42と出てきました.よさそうですね.

ちょっといじわるしてみる

ということで,いろいろいたずらをしてみます😈

u32 の返り値に対して,わざと String 型を返してみる

つまりこういうことです.

|> lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    // 文字列型をわざと返すようにしてみた
    "fn answer() -> u32 { String::from(\"42\") }".parse().unwrap()
}

コンパイルしてみましょう.

|> terminal

$ cargo build
   Compiling hello_procedural_macros v0.1.0 (/Users/xxx/dev/rust/hello-procedural-macros)                                                                                                                                                                                                                                                                    
error[E0308]: mismatched types                                                                                                                                                                                                                                                                                                                                  
 --> src/bin/main.rs:3:1                                                                                                                                                                                                                                                                                                                                        
  |                                                                                                                                                                                                                                                                                                                                                             
3 | make_answer!();                                                                                                                                                                                                                                                                                                                                             
  | ^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                                                                                             
  | |                                                                                                                                                                                                                                                                                                                                                           
  | expected u32, found struct `std::string::String`                                                                                                                                                                                                                                                                                                            
  | expected `u32` because of return type                                                                                                                                                                                                                                                                                                                       
  |                                                                                                                                                                                                                                                                                                                                                             
  = note: expected type `u32`                                                                                                                                                                                                                                                                                                                                   
             found type `std::string::String`                                                                                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                                                                                                                                
error: aborting due to previous error                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                                                                
For more information about this error, try `rustc --explain E0308`.                                                                                                                                                                                                                                                                                             
error: Could not compile `hello_procedural_macros`.                                                                                                                                                                                                                                                                                                             

To learn more, run the command again with --verbose.

なるほど,型が違うというのは教えてくれました.しかし,マクロの中のどこの関数のどの箇所で違う,というところまではさすがに教えてはくれない感じですね.エラー解析が大変なんですかね.

関数名を変えてみる

関数名を answer から別のものに変えてみましょう.関数が見つからないと言われることを期待しました.

|> lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    // 関数名を変えてみた
    "fn result() -> u32 { 42 }".parse().unwrap()
}

すると,

|> terminal

$ cargo build
   Compiling hello_procedural_macros v0.1.0 (/Users/xxx/dev/rust/hello-procedural-macros)                                                                                                                                                                                                                                                                    
error[E0425]: cannot find function `answer` in this scope                                                                                                                                                                                                                                                                                                       
 --> src/bin/main.rs:6:20                                                                                                                                                                                                                                                                                                                                       
  |                                                                                                                                                                                                                                                                                                                                                             
6 |     println!("{}", answer());                                                                                                                                                                                                                                                                                                                               
  |                    ^^^^^^ not found in this scope                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                                                                
error: aborting due to previous error                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                                                                
For more information about this error, try `rustc --explain E0425`.                                                                                                                                                                                                                                                                                             
error: Could not compile `hello_procedural_macros`.                                                                                                                                                                                                                                                                                                             

To learn more, run the command again with --verbose.

main 関数の中で呼び出している answer() が見つかりませんでした.期待通りの結果でしたね.マクロ内の関数名の一致不一致もコンパイル時に静的に解決できているという点でいいですね.

全然違う言語の文法を入れてみる

Scala っぽい文法を持った関数をわざと記述してみましょう.

|> lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    // Scala 風の関数を記述してみる
    "def answer(): Int = 42".parse().unwrap()
}

多分 parse に失敗するはず.コンパイルしてみましょう.

|> terminal

$ cargo build
   Compiling hello_procedural_macros v0.1.0 (/Users/xxx/dev/rust/hello-procedural-macros)                                                                                                                                                                                                                                                                    
error: expected one of `!` or `::`, found `answer`                                                                                                                                                                                                                                                                                                              
 --> src/bin/main.rs:3:1                                                                                                                                                                                                                                                                                                                                        
  |                                                                                                                                                                                                                                                                                                                                                             
3 | make_answer!();                                                                                                                                                                                                                                                                                                                                             
  | ^^^^^^^^^^^^^^^                                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                                                                
error: aborting due to previous error                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                                                                
error: Could not compile `hello_procedural_macros`.                                                                                                                                                                                                                                                                                                             

To learn more, run the command again with --verbose.

期待通りでした!

TokenStream の中身とは?

気になりました.調べました.

TokenStreamTokenTreeイテレータとなっています.TokenTree は enum になっていて,4つのノードを持っています

  • Group: TokenStream をカッコで囲ってグループとみなせる何かっぽい.
  • Ident: 変数名を表現している.
  • Punct: +. などで始まるオペレータと,複合記号のオペレータ (>> とか => とか) がここに含まれる.
  • Literal: char, string, あるいは数字を表現している.

ということは,中でイテレータを回しながら字句解析結果を見て,中身を定義して返せるという感じなんですかね.

まとめ

  • とりあえず挙動を確認した感じ,中で Rust の文法らしいものの字句解析と,中でちょっとした関数の紐づけ付等を行う感じの処理が走っているみたいです.が,全容がよくわからなかったです.
  • TokenStreamの 字句解析結果をさらに自分で定義できる感じですかね?たとえば,Rust の for 文を回す際に,イテレータの変数名を渡すと自動でそのイテレータに対する任意の処理を行って返すみたいな実装が考えられそう.
  • まだまだ機能がありそうですし,実際 Rust の関数以外の文法をもった何かを突っ込むこともできそうな雰囲気がありますが,使い方がまだイマイチわかっておらずよくわからないですね.

よくわからないばかりですみません.良いお年を!