Don't Repeat Yourself

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

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 の関数以外の文法をもった何かを突っ込むこともできそうな雰囲気がありますが,使い方がまだイマイチわかっておらずよくわからないですね.

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

『A PHILOSOPHY OF SOFTWARE DESIGN』 の1章〜3章を読んだ

こういう本好きなのと,話題になっていたので買ってみて真面目に読んでみています.

読みながら感想等々書いていくと頭に残りそうなのでやってみます.ただあんまり要約すると,それはそれでネタバレ感がすごいので続きは本書をご購入の上ご覧くださいということでお願いします.いい本です.

4章くらいまで読んだんですが,一旦1〜3章がひとつの意味上の区切りとしてピッタリそうなので,ここまでの3つをまとめておきます.

1章

本の導入なので割愛.英語の本って1章のイントロダクションで丁寧に本の全体像を解説してるので読みやすいですよね.

2章

ここで Complexity という本書のコアとなる概念が登場してきます.どう翻訳するのがベストかはわかりませんが,Complexity = 複雑性と翻訳させてください.

本書では,複雑性は次のように定義されます.

Complexity is anything related to the structure of software system that makes it hard to understand and modify the system.

「複雑性とは,システムを理解・変更することを困難にするソフトウェアシステムの構造に関連するものです」とのことです.要するに,コードは読めないし,コードがスパゲッティで修正が難しくなっていることを複雑性と呼んでいる,くらいの理解で読みすすめていくことにします.

後半で,複雑性の具体的な症例として次の3つが載っていました.

  • Change amplification: 変更箇所がめっちゃ増える,くらいの意味だと思います
  • Cognitive load: 直訳すると「認知の負荷」っていうことになるんでしょうけど,要するに「コードが読めん,わからん」ということです.
  • Unknown unknowns: あるコードを修正する際に,どの条件を満たしたらそのコードを修正しきったかが把握できない状況のことをいうそうです.

ソフトウェアが複雑になる要因が解説されたあと,複雑性は増え続けるんだよという説明がなされて2章は終わりました.

今回の本では,この複雑性という概念が肝になってくることを予感させる章でした.そして,この複雑性をいかに減らしていくかという方針について,本書の残りの部分が割かれている構成になっていそうですね.ソフトウェアデザインの方針は,何を目標にして設定すればよいのか迷いがちで,だからこそ「複雑性の抑制」に焦点を当てたのはとてもいいなと思います.

3章

動くコードを書けばOK,なんじゃなくて,ちゃんとシステム全体を見通したコードを書くようにしようねという話.

用語が2つ投入されているので紹介しておきます.

  • Tactical Programming (戦術的プログラミング): 動けば OK というプログラミングスタイル *1
  • Strategic Programming (戦略的プログラミング): working code isn't enough (コードは動くだけじゃダメだ).長期的な視野をもって,よいソフトウェアデザインを追求していこうというプログラミングスタイル.

Strategic Programming をしようね,という話です.せやな.あとは,Strategic Programming への投資効果について書かれています.

Strategic Programming する上ではマインドセットも必要とのことです.それはそうですね.Strategic Programming を一番効率よく推し進める方法は,エンジニアみんなでちょっとずついいデザインになるようにコツコツ投資することです.

ここまでで,本書で重要視することがコンパクトにまとめられていました.4章以降は具体的にどのようにしたら,複雑性を抑制でき,戦略的プログラミングをできるのだろうか?という話が書いてあるように見えます.読み終わったらまた,まとめと感想を書きます.

*1:working code is enough,という対になる文章をあてることもできるでしょう.「戦術的プログラミング」と翻訳するとどうも,「短期的な視野」感が失われる….

AWS Lambda の新機能 Custom Runtime を Rust でトライ

f:id:yuk1tyd:20181201203101j:plain

この記事は CyberAgent Developers Advent Calendar 2018 3日目の記事です.

アドテクスタジオ所属の yuki です.社内の方向けに軽く自己紹介をしておくと,2017/11 中途入社です.アドテクスタジオの某プロダクトでテックリードをしています.

Rust が好きなので,基本社内でも社外でも Rust の話しかしていません.Rust のゼミを最近同僚の方と一緒に立ち上げるなど,Scala や Go 言語の採用事例の多いアドテクスタジオ内で,Rust の市民権を得ようと (笑) がんばっています.

先日,ついに念願の待ちに待った, Rust によって記述された関数を AWS Lambda 上で実行可能になったというアナウンスがありました.Custom Runtime という機能です.実はこれまでにも Lambda 上で Rust を実行する方法はあったのです.が,完全に Rust サポートが達成されたので,これからユーザーは Custom Runtime を使っていくことになるでしょう.

ちなみにこれまで Rust では,

このように wasm を介して実行してみたり,あるいは

Go 言語のバイナリになりすましてみたりと,さまざまな方法で実行が試みられてきました.だがしかし!Rustacean はこれで,自身の大好きな Rust で Lambda を実行できるようになったのです!すばらしい世の中になりました.

話がそれてしまいましたが,まず新機能がどういった機能なのか,概要を説明していきます.つぎに,Rust がどのような言語かをご存知ない方も多いかと思いますので,Rust がどういった言語かについて説明します.そのあと,実際に Rust によって記述された関数を Lambda 上で動かしていくことにしましょう.

AWS Lambda Custom Runtime とは?

2018 年の re:Invent で発表された新機能です.要するにさまざまな言語で Lambda を動かすことができますよという環境です.

f:id:yuk1tyd:20181201200419p:plain
従来の言語に加えて,「独自のランタイムを使用する」という項目が追加されている

これを使用するためには,Lambda 上で動かすために必要な処理が書かれた bootstrap というバイナリファイルを用意し,それを zip に固めて従来どおりアップロードするだけです.

Rust とは?

f:id:yuk1tyd:20181201201252p:plain

Rust は近年注目度の高まってきている言語です.StackOverflow の愛され言語ランキングでもここ数年トップを取り続けています.大きな特徴としては次のような点があげられると思います.

所有権,借用,ライフタイム (Ownership, Borrowing, Lifetimes)

Rust の代名詞といってもいいかもしれません.

所有権やライフタイムという仕組みによる強力かつ安全なリソース管理が可能です.メモリだけでなく,ネットワークコネクションの管理も自動的に行います.安全性の満たせないプログラムはコンパイル時に拾い上げられます.

GC がないという特徴を聞いたことがある方もいるかもしれません.それはこの3つの概念によって,安全にメモリの確保・解放を管理しているがゆえに実現されています.メモリ管理に関して,プログラマが余計なコードを追加することはありません.

強力な型システム

型によるリソース管理が行われます.したがって,メモリ安全でない操作はコンパイル時に検出されます.他の言語ではランタイム時に解決されるような問題が,Rust においてはコンパイル時に解決されてしまいます.コンパイラが強い味方です.

また,強いて言うならば型クラス指向の言語で,それゆえに抽象化の力が強いです.抽象化の威力を存分に発揮した柔軟なソフトウェアデザインが可能です.型推論もほとんど完璧に行われるため *1,型を記述する場面はほとんどありません.

あらゆる箇所に型をつけていこうという強い意志が感じられます.またそれを利用した RustBelt のような定理証明系による支援プロジェクトも活発に行われています.強力な型システムに支えられた並行・並列処理への強さも魅力のうちのひとつです.

システムプログラミング言語である

システムプログラミング言語なので OS が作れます.「誰もがシステムプログラマーになれるように」というのが,次の Rust のキャッチフレーズに選ばれたとおり,Rust を使うことで誰もがシステムプログラマーになれます.

最高クラスのパフォーマンスとゼロコスト抽象化

もちろん時と場合によりますが,大抵のケースにおいて,かなり高速なプログラミング言語である Go 言語よりもさらに速くC++ とほぼ互角のパフォーマンスを発揮します.その要因のひとつは,安全性に重きをおきつつゼロコスト抽象化にも妥協しておらず,実行時の余計なオーバーヘッドが発生しないためです.ビビるくらい速い ("blazingly fast") と公式ドキュメントで謳っている通りです.

Cargo

Rust には最初からパッケージマネージャとビルドツールがついています.cargo です.cargo を使用するだけで,ライブラリの依存関係の解決やビルドをすべて cargo xxxコマンドラインで打つだけで実行できます.他言語にはよくあった「どのビルドツールがいいのか?」問題は,Rust では発生しません.cargo 一択だからです.

もちろん,cargo fmt と打つだけでフォーマッタも走ります.したがって,Rust ではフォーマット問題も発生しません.

温かいコミュニティ,ドキュメントの親切さ

Rust の最大の資産になりうるのは,私は温かいコミュニティだと思います.Rust コミュニティには,「Rust は難しい」という自覚が (おそらく) あり,入門者の方が少しでもスムーズに入門できるように,さまざまな工夫の凝らされたドキュメントが豊富に用意されています.OSS でもドキュメント,あるいはコード内のコメントや Spec 用のテストを丁寧に書く文化が醸成されており,初めて使うライブラリの使い方がわからない…という場面に遭遇することが少ないように思います.

Custom Runtime を使ってみる

ドキュメントに沿ってやってみようと思います*2.使用環境は Mac OS X です.エディタは CLion を使用しています.今回参考にしたドキュメントはこちら

toml の準備

サンプルプログラムでは次の crate を使用しますので,Cargo.toml に設定を追加します.

  • lambda_runtime: AWS 提供の Lambda Runtime が記述されたライブラリ.
  • serde, serde_json, serde_derive: Rust ではおなじみの JSON のパースを行うためのライブラリ.
  • log, simple_logger: ロギング機構.

また,Lambda に読み込ませるためにバイナリファイルの設定が若干必要なので,それも行いましょう.完成した Cargo.toml です.

[package]
name = "rust-lambda-testing"
version = "0.1.0"
authors = [{your author name}]

[dependencies]
lambda_runtime = "^0.1"
serde = "^1"
serde_json = "^1"
serde_derive = "^1"
log = "^0.4"
simple_logger = "^1"

[[bin]]
name = "bootstrap"
path = "src/main.rs"

クロスビルドのための準備

Lambda の環境に合うようにクロスビルドを行う必要があります.Mac の場合の設定方法は公式ドキュメントに載っていました.とても親切ですね.それに従って設定していきましょう.

まず,rustup ツールチェインに x86_64-unknown-linux-musl 向けの設定を追加します.

$ rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
 14.9 MiB /  14.9 MiB (100 %)   3.1 MiB/s ETA:   0 s                
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'

次に,Mac であれば x86_64-unknown-linux-musl 向けのリンカを brew を経由して入れる必要があるので,入れておきます.

$ brew install filosottile/musl-cross/musl-cross

最後に .cargo/config に設定を追加しておきます.

mkdir .cargo

config ファイルを作成して,

[target.x86_64-unknown-linux-musl]
linker = "x86_64-linux-musl-gcc"

と書いておきます.

これで準備は整いました.それでは,コードを軽く書いてビルドし,実際に動かしてみましょう.

コード

とりあえず動かしてみたいので,公式サンプルをそのまま使います.

#[macro_use]
extern crate lambda_runtime as lambda;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate log;
extern crate simple_logger;

use lambda::error::HandlerError;

use std::error::Error;

#[derive(Deserialize, Clone)]
struct CustomEvent {
    #[serde(rename = "firstName")]
    first_name: String,
}

#[derive(Serialize, Clone)]
struct CustomOutput {
    message: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    simple_logger::init_with_level(log::Level::Info)?;
    lambda!(my_handler);

    Ok(())
}

fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
    if e.first_name == "" {
        error!("Empty first name in request {}", c.aws_request_id);
        return Err(c.new_error("Empty first name"));
    }

    Ok(CustomOutput {
        message: format!("Hello, {}!", e.first_name),
    })
}

ビルド & アップロード用の zip 生成

--release ビルドをしましょう.最適化が走り,Rust 本来の力を発揮できるためです.

cargo build --release --target x86_64-unknown-linux-musl

ところで私の環境でビルドすると,musl-gcc がないと言われました.先ほどインストールした brew の musl が認識されていなかったようです.次のようなエラーが出てしまい,ビルドに失敗しました.

--- stderr
thread 'main' panicked at '

Internal error occurred: Failed to find tool. Is `musl-gcc` installed?

', /[root dir name]/.cargo/registry/src/github.com-1ecc6299db9ec823/cc-1.0.25/src/lib.rs:2260:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

対策は公式ドキュメントにしれっと書いてありますが,

ln -s /usr/local/bin/x86_64-linux-musl-gcc /usr/local/bin/musl-gcc

というリンクを設定しておくことで回避可能です.こうすると,ビルドが通ります.

次に Lambda アップロード用の zip ファイルを作ります.

zip -j rust.zip ./target/x86_64-unknown-linux-musl/release/bootstrap

すると,このような zip ファイルが作成されるかと思います.これを Lambda にアップロードすることで,関数を実行することができます.

f:id:yuk1tyd:20181202165007p:plain
rust.zip が生成されました

実行

上記の zip ファイルを Lambda にアップロードします.その後,テスト関数に次のような JSON を入力し,テストを実行します.

{
  "firstName": "Rustacean"
}

まとめ

  • AWS Lambda Custom Runtime のおかげで,ついに Rust を Lambda で動かす環境が正式にサポートされました.
  • Rust のサンプルプログラムを作って遊びたい際に,Lambda 上で一度遊んでみるという選択肢が増えたという点でとてもすばらしいと思います.
  • もう少し重ためのプログラムを動かして,Rust の威力を存分に味わってみたいなと思いました→と思ったら,Rust の Advent Calendar の方ですでに計測してくださった方がいました.Go 言語と遜色ない性能がでているようですね.(追記あり) AWS Lambda が正式に Rust 対応したので KinesisFirehose にくっつけて性能計測した #rustlang #rust_jp - ソモサン

*1:Hindley-Milner ベースだが,ライフタイムのサポートがあるので完全なそれではありません.

*2:ここからは,Rust をすでに使用したことのある方を対象として書きすすめていきます.Rust のインストールなどは,Rust の公式サイトをご覧ください.また,詳しいプログラムの解説については,今回参考にした公式ドキュメントにかなり突っ込んで書いてありますので,そちらもあわせてご覧ください.

Shinjuku.rs で actix-web の話をしました (ちょっとした解説付き)

11/21 に Shinjuku.rs で登壇しました.今回は actix-web に関する話をしました.もちろん LT ではすべてを話しきることができませんでしたので,今回も裏話を記事にして書き留めておこうかと思います (自身の頭の整理にもなるためです).

forcia.connpass.com

基礎知識編

同期 I/O vs 非同期 I/O,ブロッキング I/O vs ノンブロッキング I/O

非同期 I/O については,ユーザー数の多いサービスや,あるいはアドテクなどではとくに意識することが多いのですが,案外他の種類のアプリケーションを作っている方には馴染みのない概念かもしれないと思い説明を加えました.ただ実はこれがすべてというわけではなく,他の対となる概念と比較対象しないとよさや何をしたいかがよくわからないものでもあります.なので,今回の記事では,非同期 I/O について学ぶ際に登場してくる (であろう) 4つの概念を一気に説明していきます.

まずこの話を始める際の前提として,2人の登場人物が存在します.

  • プロセス
  • OS

プロセスが OS に I/O タスクを投げます.このイメージを忘れないでください.

プロセスが OS にタスクを投げ,そのタスクに関するなんらかの返答を受け取る方式の種別が同期・非同期です.同期 I/O の場合は,OS に I/O タスクを投げ,入出力の準備ができたらアプリケーションから実データを受け取ります.非同期 I/O の場合には,OS に I/O タスクを投げ,入出力の準備ができたら通知を受け取ります.両者の違いはそこにあります.

プロセスが OS にタスクを投げ,そのタスクの結果をどのように受け取るかの種別がブロッキング・ノンブロッキングですブロッキング I/O の場合には,プロセスが依頼したタスクを OS が終えるまで,プロセスはその結果を待ちます.一方でノンブロッキング I/O に場合には,プロセスは依頼したタスクを OS がこなしている間,別の処理をこなすなどしてその返りを待ちません.OS から通知があった際に結果を受け取ります.

そして,2つの種別は掛け算になります.つまり,「同期・ブロッキング」「同期・ノンブロッキング」「非同期・ブロッキング」「非同期・ノンブロッキング」の4種類の方式が存在します.それら4つの方式は,それぞれ異なるシステムコールを使用します.表にすると下記のようになるでしょう:

ブロッキング ノンブロッキング
同期 read/write read/write (O_NONBLOCK)
非同期 select/poll (Linux なら epoll を使うなど) AIO

ここまでの話をまとめておきます.

  • 同期 I/O は OS にタスクを投げたあと,実データを受け取る.
  • 非同期 I/O は OS にタスクを投げたあと,通知を受け取る.
  • ブロッキング I/O はタスクを OS に依頼するとその結果を待つ.
  • ノンブロッキング I/O はタスクを依頼しても結果を待たず別の処理をこなす.OS から通知があって初めて結果を受け取る.
  • 同期・ブロッキングreadwrite というシステムコールを使用する.
  • 同期・ノンブロッキングは,readwrite というシステムコールを使用するが,O_NONBLOCK というフラグがついている.
  • 非同期・ブロッキングselectpoll というシステムコールを使用するが,効率が悪いので epollkqueue などを用いる.
  • 非同期・ノンブロッキングは AIO というシステムコールを用いると言われているが実装が成熟していないらしい.

並行処理と並列処理

これはいわゆる言葉の定義の問題になってきて,前提次第によってはさまざまな定義が出てきてしまうかもしれませんが,一般には次のように区別されることが多いです.

  • 並行処理: CPU数,コア数の限界を超えて,複数の仕事を行うこと (1コア内で複数処理を行うことをイメージするとわかりやすいです)
  • 並列処理: 複数の処理を同時に起こすことによって,効率よく仕事を行うこと

並行処理がわかりにくいと思うので簡単に解説します.簡単化のために,CPU を1コアだと仮定します.1コアだったとすると,たとえばブラウザを開きながら調べごとをしつつ, IntelliJ を使ってプログラミングを行うということは現実的に不可能なように思えます.が,これは並行処理を使うと仮定するとできます.なぜかというと,1コア CPU の中で,瞬間的にブラウザの処理と IntelliJ の処理を切り替えながら処理を行っているためです.

人間が気づかないくらい短い間隔で,複数の処理を切り替えながら実行しているのです.これが,並行処理が論理的に複数の処理を実行可能であるゆえんです.

並列処理はそうではなくて,物理的に同時に複数の処理を行います.つまり,そのまま複数人で複数の仕事を行うということです.なお,並列処理は並行処理の中に含まれます.

参考

いくつかあるとは思いますが,私は次の本に載っている内容で理解しました.

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

アクターモデルについて

アクターモデルに関する説明は,上述のスライドの中で十分かなとは思っています.Akka に関する説明ではありますが,次の記事が図も説明もとてもわかりやすいと思うので,イマイチイメージが掴めなかった方はぜひご覧ください.

enterprisegeeks.hatenablog.com

ところで,アクターモデルのいいところとは一体なんでしょうか?並行処理をする上で避けては通れない問題の中に,いわゆる競合状態というものがあります.アクターモデルは,その競合状態に対する有効な解決策として考案されました.

競合状態は銀行口座の例がよく出されるのでそれを使用して考えてみます.次の銀行口座をイメージした疑似コードをご覧ください.

もし預金残高が $100 以上あれば {
    預金残高 - $100
    $100 分のお金を引き出す
}

シングルタスクの状況下においては,上記のプログラムは常に正しく動くはずです.しかし,マルチタスクのは以下ではどうなるでしょうか.次のような状況を考えてみましょう.

(預金残高が $150 あるとします.まず,プログラム A が走っているものとします)
A: 預金残高が $100 以上あるか?→YES
(プログラムを切り替えます.プログラム B に切り替わります)
B: 預金残高が $100 以上あるか?→YES
B: 預金残高 - $100 して,$100 分のお金を引き出す
(預金残高は $50 になっています.ここで,プログラム A に切り替わります)
A: 預金残高 - $100 して,$100 分のお金を引き出す
(あれ…?すでに残高は $50 しかないのに,$100 引き出されてしまった〜!不整合発生!)

このような状況のことを競合状態 (race condition) と呼びます.並行処理で同期的な処理を求められる場合に起こることが多く,スレッドセーフでない実装という呼び方をしたりもします.

競合状態は,2つ以上の処理が変数を共有しているような場合に起こります.上述の例ですと,預金残高という変数を,プログラム A とプログラム B が共有しているために競合状態が発生しました.

よくある解決のアプローチは,Lock (あるいは Mutex や Semaphore) を用いる方法です.メモリ共有されている箇所にアクセスする際には必ずロックを用いてアクセスするようにするということです.Java などではこちらをよく用いますね.Rust でも,Mutex が用意されている通りです.しかしロックにはロック特有の問題があります.デッドロックです.他にもいろいろな問題点があります.

あるいは,Rust でも登場してきますが,メモリを共有するとしても書き換えを不可能にしてしまえばいいのではないか?というアプローチを取る場合もあります.イミュータブルなものを扱おうという思想です.最近では多くの言語にこの方法も取り入れられてきているように思います.

アクターモデルのアプローチはロックではなく,メモリをそもそも共有させないことでした.アクターはそのかわりにメッセージを送ることで解決をはかりました.メッセージを投げるだけ投げて,相手側の処理は非同期的に行わせ,最終的に終わったときに終わったメッセージを受け取ることで処理を完了するようにしました.

アクターモデルにデメリットはあるのか?という話ですが,なくはない (でも大体の場合は回避策が用意されているものです) ようです.私は正直概要をさらっと知っている程度です.次のスライドが参考になるかなと思いましたので,興味のある方はご覧ください.

https://techno-tanoc.github.io/ex_slide/#/

※ Elixir は Erlang VM の上に乗っている Ruby のような文法をもった新しい言語です.

tokio

この記事がとても詳しいです.私もこの記事で理解をしました.

Tokio internals: Understanding Rust's asynchronous I/O framework from the bottom up : Caffeinated Bitstream

tokio, mio, future の関係性なども完璧に説明されておりすばらしいです.

まとめ

同期・非同期,あるいは並行処理なのか並列処理なのかといった微妙な話題を取り扱ったので,簡単にまとめとして記事を書いてみた次第です.説明が足りない部分については,ぜひ私にダイレクトメッセージをください.正直この分野は私もまだ勉強中です.

actix-web の内部実装についても軽く見てみたのですが,結構興味深かったので後日あげようと思います.

リポジトリ

github.com