技術書典5にて、Sinatraのコード解説本を頒布します。あと執筆環境の話。


技術書典5に参加します

2018/10/08 に池袋サンシャインシティで開催される技術書典5にサークル参加します。すぺーすは「い22」です。よろしくお願いします。

頒布する本の内容は、ここ半年ほど作っていたMobbというボットフレームワークのコードを書くために読み込んだSinatraの、コードリーディングと解説の本です。Sinatraは非常にミニマルながらも強力な黒魔術と設計に支えられた、Ruby力を上げる教材としてとても良い題材だと思ったので、その知見を広く共有するために本にしました。

本の詳細と言い訳

頒布する本は、本文44ページで頒布価格は1000円にする予定です。これは、印刷した本の45%が売れた場合に、印刷代と参加費、その他諸経費がペイするように設定しました。45%の根拠は、同じサークルの檻総君が夏と冬に刷っている同人誌の売れ行きを観察するに、これくらいの数値設定が妥当だとおもわれるためです。

本文は、おおよそ二週間かからずに書き終わりました。しかし、その特急スケジュールのおかげで文字以外の図解などを作る暇がなく、やや読んでて疲れる本になってしまったという自覚があります。今回の技術書典でこの本を求めてくれる人が多ければ、もっと読みやすくした版を後日作成し、本を買っていただいた方には何かしらの形で無料で配れればと思ったりもしますが、その思いを実行できるくらい当日売れることを祈るばかりです。

本の執筆環境

今回、初めて技術書同人誌を書くにあたって、どのような環境で書こうかと考えていました。実現したかった要件は次の通りです。

  • markdownでの執筆
  • クラウドエディタでどこからでも書ける
  • Re:VIEWで組版

そのため、markdownの補助があるクラウドエディタで執筆し、その後にmd2reviewで変換、という方法を模索しました。

tl; dr

StackEdit+GoogleDrive+md2reviewでやろうとおもったけど、なんか全体的にあんまりうまくいきませんでした。特に、md2reviewでの変換で完全に詰まってしまい、最終的にGitBookを使用しました。

markdownエディタ

クラウドで使えるmarkdownエディタはいくつかありますが、今回はStackEditを選択しました。他にもesa.ioも検討しましたが、次の理由でStackEditを選択しました。

  • 同期にGoogleDriveが使える
  • mdのライブプレビューをオンオフできて全画面で書ける
  • esa.ioはナレッジを貯める場所であり、長文の原稿を貯める場所としては適していない気がする(気がする)

しかし、ライブプレビューに関しては特に不満はありませんでしたが、GoogleDriveでの同期や、そもそもStackEditの完成度に関して大きく不満の残る結果になりました。

そもそもGoogleDriveをバックエンドに選択したかった理由は、markdownエディタを使用できないオフライン環境でも、iPadなどのDriveアプリから直接ファイルを編集したり参照したかったためです。しかし、StackEditは同期のバックエンドにGoogleDriveを選択できるだけで、ファイルへの直接のアクセスは出来ませんでした。そのため、タブレットやスマホから原稿を確認することが出来なかったのは大きな誤算でした。Safariでブラウザ版のDriveからStackEditを起動することもできず、不便でした。ただ、あとから気づいたことですが、StackEditを起動するとドメインがグーグルから移動したので、もしかしたらStackEditに直接アクセスしたら利用できたかもしれません(確認してないので不明です)。

他にも、StackEditはエディタとしてもあまり性能が良いとはいえず、とくにmarkdownの入力補助はほとんどありませんでした。また、起動するたびに勝手にファイルが増えたり、しょっちゅう同期が壊れたりなどで、とにかく不安定でした。恐らく、もう使うことは無いと思います。

markdownからRe:VIEWへの変換

md2reviewというgemがよく使われていると聞いたので、それを使う予定でした。しかし、実際にmdで原稿が完成したのちに変換をかけてみると、エラーが出て変換が出来ませんでした。何でだろうと思ってgemの中身を覗いてみましたが、いまいちよくわからず、印刷所の締め切りの3時間前の段階ではとても追跡する気にはなれませんでした。md2reviewのリポジトリを見てみると、最後のコミットが10ヶ月前であまり活発に開発されているとは言い難い状況だったので、諦めて他の手段を模索しました。

GitBook

入稿締め切りの3時間前に、藁にもすがる気持ちでmarkdownをPDFに変換してくれるソリューションを探したところ、GitBookに行き着きました。原稿のmdをそのままGitBookに渡すと、思った以上にすんなりとPDFに変換してくれたため、チュートリアルのようなものを読んで急いで原稿とSUMMARY.mdを編集し、出力しました。

成果物は、思いっきりどこかで見たことあるような装丁で、いろいろと直したいところもありました。しかし、窮地を救ってくれたGitBookには感謝の気持しかありません。

次回の執筆環境をどうするか

後日会社でいろんな人に聞いて回ったところ、多少の差異はあれ概ね次のような分類の環境が多かったです

  • markdown + Sphinx
  • markdown + Pandoc + laTex
  • 最初からTeX
  • 最初からRe:VIEW

最初からTeXやRe:VIEWもまあ無いことはないと思うのですが、どの環境でもライブプレビューなどを使いながら書くことに問題がありそうなので、あまり選択したくは無いと思います。

やはりMarkdownを利用したいので、SphinxかPandocを利用することになりそうです。この二つであれば、最後の組版の部分はどの環境でもというわけにはいかないでしょうが、原稿が完成するまではmarkdownで書くことができます。

また、エディタをどうしているかというのを社内で聞いたところ、ほぼ全員が「クラウドで同期とかどうでもよくない? 本当にどこでも書きたいの?」という意見だったので、なんだか自分の要求が特殊すぎる気がしました(実際、今回の原稿は自宅のPC、仕事のノート、私物のノートの3台の間を行ったり来たりしながら書いていたので、確実に自分は必要だと思うのですが……)。

エディタの部分はどうするか色々考える必要はありますが、次回の執筆にはSphinxかPandocを使おうと思っています。幸い、技術書典にはいくつか技術書を書く技術の本が出るので、それを買い求めようと思っています。

Sinatra本よろしくお願いします

よろしくおねがいします。


カテゴリー: ポエム, 未分類 | コメント / トラックバック: 0個

Mobb 0.3 をリリースしました、これで実践的に毎秒クソボットをリリースできます


Mobb 0.3.0 をリリースしました

今回のリリースでは、 cron/every キーワードが Mobb DSL に追加されています。

https://github.com/kinoppyd/mobb/pull/5

これらのキーワードは、 Mobb(およびRepp) になにかを定期実行させるためのものであり、このリリースによってようやく Mobb は実用的で最も簡易な Bot フレームワークを名乗ることができるようになったと思います。

最初に Mobb を作ろうと考えたときに、秒でクソボットを作れるようになるために必要なものはなんだろうと考えました。そして、必要だと考えたのは、シンプルなシンタックスで書けること、条件分岐がフレームワークの機能に備わっていること、なにかを定期実行できること、の三点でした。 Mobb 0.3 のリリースにより、これら3つの要素が全て揃い、ようやく Mobb が実用的な Botフレームワークとして皆さんに使ってもらえる機能を備えました。

Syntax

cron/every キーワードは、名前の通りなにかを定期実行するためのものです。具体的なシンタックスは、次のように書きます。

この例では、毎日12時から21時までの毎事0分に、botが times_kinoppyd という配信先(Slackであれば #times_kinoppyd) に対して、 “Hi bro, wazup?” というメッセージを配信します。

cron キーワードは、そのまま Cron の文法が使えます。具体的には、 parse-cron というGemを使ってパースできるものであれば動作します。

また、 cron の簡易的な使い方を目的として、 every というキーワードも追加しています。

cron よりも、より直感的に定期実行を記述できます。これも、 whenever というGemを内部で利用しており、 whenever が解釈できる引数をそのまま渡すことができます。例えば次の例では、2というIntegerオブジェクトへのWheneverの拡張により、hoursというメソッドが追加されていて、それをそのまま使用することができます。

every キーワードが受け取る事のできるシンタックスに関しては、 whenever のREADMEを参考にしてください。

注意点として、 cron/every キーワードでは、必ず dest_to というコンディションが必要な点に注意してください。これを設定しないと、Botはどこのチャネルにポストすれば良いのかがわからず、メッセージが闇に消えることになります。(Shellアダプタなど、チャネルの区別が無い環境であれば必要ありません)

cron/every の実装

この機能の実装には、非常に頭を悩ませる次のような問題がありました。

  1. この機能はMobb(アプリケーションフレームワーク)が持つのか、Repp(Botフレームワークインターフェイス)が持つのか
  2. どちらに持つとしても、実際にどのような実装にするのか
  3. Sinatra-ishなDLSや実装とどのように共存するのか
  4. cron文法の解釈に、どのGemを選択するのか
  5. 実装に際して、スレッドの管理にはどのような方法をとるのか

Mobbの機能なのか、Reppの機能なのか

cronは、Mobbの実装です。しかし同時に、Reppの実装でもあるべきです。

Repp に関して簡単に説明すると、 Mobb が Sinatra だとすると Repp は Rack です。

まず大前提として cron の文法は、 Mobb アプリの定義に書かれます。何故かと言うと、ユーザーが書くのはMobb アプリケーションであり、Repp ではないからです。しかし、 cron の定期実行の機能自体をどちらに書くかは、大きな問題です。

この問題に関して、 Mobb と Repp は、Mobb が cron の解釈を行うが、 Repp が定期実行のトリガーを送信する、という方法をとって実装をすることになりました。これは、 Mobb がアプリケーションフレームワークであるのに対して、 Repp がインターフェイスであることが理由です。実際に定期実行を行う方法はアプリケーションに任せるが、そのトリガーとなるイベントはインターフェイスである Reppが提供するということです。

また、 Repp が送信するトリガーも、通常のチャットサービスから送られるメッセージ類と区別しないようにしました。つまり、 Repp の世界観に置いて、すべてのイベントはアプリケーションの call メソッドを呼び出すことで実行されます。0.2 までのリリースでは、 Mobb のコードにはコメントアウトされた tick というメソッドが存在していました。しかし、 0.3 ではそのメソッドは消去されています。これは何故かと言うと、Repp が Rack を参考にしているためで、 Rack はアプリケーションの最小単位として 一つの引数を受け配列を返すcall メソッドを持つオブジェクト(つまり、 Proc オブジェクトが事実上の最小単位)として定義しています。Repp でも、 Mobb のような FW を使うことなく Bot を記述できるインターフェイスであるべきだと思い、このような形にしました。

定期実行をどのような実装にするのか

定期実行のトリガーそのものは、 Repp に実装することに決めました。しかし、どのように実装するのでしょうか?

Repp 0.3 では、この実装の一つの実験として、毎秒Tickerイベントをアプリケーションに送信するという方法をとっています。Tickerイベントは、イベントが発生した時間を同時に送ります。

これをどのように解釈するかはアプリケーション側の自由ですが、 Mobb では Ticker イベントを受け取ると、 cron/every で定義されたイベントと一致するかを確認した上で、イベントを実行します。

cronの文法(定期実行の最小単位が毎分)を使っているのに、なぜ毎秒イベントを送っているかというと、あくまで Mobb は cron の文法を採用しただけであり、 Repp はただのインターフェイスなので、可能な限り細かい単位での実行をしたほうが、 Repp を利用する他のアプリケーションに対して優しいと思ったからです。

Sinatra-ish な DLS との共存

定期実行という概念は、 Mobb が参考にしている Sinatra の世界観には存在しません。なぜなら、Webの世界では入力(リクエスト)と出力(レスポンス)がかならず対になるからです。一方で、Botフレームワークの世界では、入力と出力は必ずしも対ではありません。その一つが、この定期実行という概念です。

前述の通り、 Repp から Mobb に送られるトリガーは、通常のチャットサービスと区別をしません。なので、毎秒の Ticker イベントを処理することで、擬似的に入力と出力の対を作り出しています。

Sinatraでは、 @routes という名前のハッシュのキーに、HTTPのverb(GETとか)を使い、それぞれのverbごとにイベントを持ちわけています。同じように Mobb でも、 Repp から来るイベントの種類によって、 @events という名前のハッシュにイベントを持ちわけています。具体的には、チャットサービスからくる message というイベントと、 Repp の定期実行タイマーからくる Ticker というイベントです。

これにより、 Repp から届くイベントを混線させることなく振り分けることが可能になっています。

どのGemを選択するか

cron文法のコンパイル、つまり every キーワードで簡易的に cron 文法を扱うためには、 whenever というGemを使用しており、そのほかに選択肢はありませんでした。しかしながら、 whenever そのものはcrontab を効率的に編集するツールであり、自分が欲しかったのは cron syntax のコンパイラ部分だけだったので、これを採用するかは少し悩みました。幸い、whenever はMITであるため、cron syntax を生成する部分だけを自分のコードに転用することも考えましたが、ひとまず最初のリリースではそれを見送り、素直に whenever に依存しています。

また、 whenever が生成した cron syntax の文字列を、Ruby で扱うために使うGemに関しては少し悩みがありました。それは、 perse-cron と、 Ruboty-cron のためにつくられた chrono という選択肢があったためです。

この選択肢で悩んだ理由は、最初の「Mobb の機能なのか、 Repp の機能なのか」という問題があります。もし cron のトリガーを Repp に持たせずに Mobb に持たせる決定をしていた場合、 chrono を選択していたと思います。なぜなら、 chrono は cron syntax を渡すと、単体で定期実行まで行ってくれるためです。

しかし、実際には Repp 側に定期実行のトリガー発行を任せることに決めたため、 parse-cron を採用しました。実際に cron syntax を操作するのは Mobb なので、 Repp 側に cronのパーサーが必要なくなり、また Mobb 側には定期実行の能力は必要なくなりました。そうなると、 chrono はオーバースペックだったため、シンプルに parse-corn を使うことにしました。

スレッド管理にはどのような方法を使ったのか

結論から言えば、今回は concurrent-ruby を使いました。しかし、これは 0.3 のリリースでの話であり、今後もこれを維持していくかは不明です。

まず最初に、Eventmachineを使う案がありました。なぜならば、 Repp が標準で組み込んでいる Slack のアダプタに使用している slack-ruby-client というgemでは、EMを内部で使用している(正確には選択できる)からです。

しかし、次の2つの理由により、EMの使用は見送ることになりました。

1つ目に、既に slack-ruby-client が使用しているEMに対して、新しく外側からなにかを追加するのは危うく見えたからです。これは私自身がEMに対する理解があまりないというのも問題ですが、まだrunしていないEMのリアクタに対して、定期的になにかを非ブロッキングで実行し続けるコードを書くことは、困難に思えたからです。実際には、おそらく何かしらの解決策があるのでしょうが、私自身の理解の不足で諦めました。

2つ目に、 slack-ruby-client の使用するEMは、正確にはEMを使用することを選択できるという言い方が正しく、実際に slack-ruby-client を使用するときに非同期用のライブラリを2つから選択することができ、その依存を gemspec に書くことになるからです。ちなみに、他の選択肢として ruby-async と celluloid を選ぶことができます。

slack-ruby-client のこの実装を見たときに、同じように Repp も長期的にはユーザーが非同期ライブラリになにを使用するかを選択できるような方針を取るべきだと考え、ひとまずEventmachineを見送ることにしました。

EMを使わないことは決めましたが、実際になにを使うかは全く思いつきませんでした。なんなら、 chrono のように自分で Thread を書いてもいいかなとも思いました。しかし、ノンブロッキングで毎秒定期実行する処理を自分の手できちんと書くことはあまり気の乗る作業ではありませんでした。

そのため、今回はEMと比較用にいろいろコードを書いて試していた concurrent-ruby を使いました。ひとまずEMに依存せず(concurrent-rubyには依存しますが)、自分の手で複雑な非同期プログラミングのロジックを書かなくていいという妥協の選択でした。

おそらく、 Repp のバージョンが上がっていく中で、この選択は見直されることになり、他の非同期の方針を模索するときが来ると思います。

まとめ

実用的なクソボットフレームワークとして成長した Mobb をよろしくお願いいたします。


カテゴリー: 未分類 | コメント / トラックバック: 0個

Mobb 0.2.0 out now


Mobb 0.2.0 をリリースしました

このバージョンでの大きな変更点は、helpersメソッドの追加とconditionメソッドの追加です。それぞれ、Mobb DSLのベースとなっているSinatraでは非常に大きな役目を果たしていたけれど、まだ移植が終わっていなかった機能です。

https://github.com/kinoppyd/mobb/releases/tag/v0.2.0

新機能の解説

0.2.0では、 helpers と condition が大きな変更点です。

helpers

helpersメソッドは、onイベントの中でアクセス可能なメソッドをMobbアプリケーションのトップレベルに定義するためのメソッドです。

このように、 on メソッドのブロックの中でアクセスできる greet メソッドのような、ヘルパーと呼ばれるメソッドを定義します。

本来、 on メソッドのブロックは、この記述の場合は main オブジェクトに対するクロージャになっています。しかし、MobbやSinatraは内部でこの binding を書き換えるトリックを使っており、通常の手順では main オブジェクトに定義したメソッドや値を参照できません。例えば、次のコードは実際にブロックの中が呼び出される時にエラーになります。

そのため、 helpers というメソッドを使い、一時的に self を on メソッドのブロックが実行される時と同じスコープに書き換えます。そして、そのスコープ内で def を使ってメソッドを定義することで、 on のブロックからアクセスできるヘルパーメソッドを定義することができるのです。

Conditions

conditionsは、 on に対して次のようなオプションを付与するように振る舞います。

この例では、 bot以外からの Yo という発言に Yo と返事をし、自分に対するリプライの中に Hi の文字が含まれていれば、 Hi と返事をします。特に Yo の方は、 ignore_bot のコンディションを有効にしないと、自身の発言に対しても Yo と反応してしまい、無限ループが発生します。

現在、デフォルトで用意されている condition は、 ignore_bot と reply_to_me で、いずれも bool の値をとります。ですが、次のようにして独自の condition を定義することも可能です。

set メソッドで probability という Condition を定義しています。ブロックの中では、  value という引数を受け取り、 condition メソッドのブロック引数の中でそれを利用しています。この例では、 Yo という呼びかけに対して1/10の確率で Yo と返し、それ以外は Ha? と返します。Mobbでは、同じマッチングの on を複数定義すると、定義した順に評価して先に一致した on のブロックを実行します。 probability という condition は、9/10の確率で失敗するので、失敗した場合は下のブロックが実行されるというわけです(下のブロックは確率的な condition が設定されていないので、必ず成功します)。

Mobbの今後の予定

周囲に使ってくれている人が何人かいるので、フィードバックを受けながら順次改修していく予定です。一旦の直近の目標としては、現在 on ブロックの中で直接触っている @env の変数を、何かしらの形でラップしようと思っています(Sinatra における request メソッドのように)。

これからもMobbをよろしくおねがいします。


カテゴリー: Ruby, プログラミング | コメント / トラックバック: 0個

技術書展5で、Sinatraのコードリーディング本を頒布します


技術書展5に当選しました

当選してしまったので、技術系の同人誌を書こうと思います。

最近の個人的な活動の成果として、SinatraのDSLっぽくBotが書けるMobbというプロダクトを作っています。そしてその過程で得たSinatraのコードリーディングの知見を広く共有するのが良いだろうと思っているので、同人誌にしようと思います。

たまたま、今日の夕方に会社のイベントでこういう発表をしたのですが、これを更に詳細に掘り下げた本になると思います。

スペースはどこ

◎貴サークル「トレイリア学園」は、 い22 に配置されました。

どういう本にするの

Sinatraの全コードの解説を目指します。一冊読めば、自分でSinatraライクのDSLを書けるようになるくらいの本にしたいと思っています。対象読者層は、メタプログラミングRubyを読んだことがあるRubyエンジニアを想定していますが、それではあまりにもリーチが狭い(気がする)ので、メタプログラミングRubyの本を読んでいなくても簡単なエッセンスくらいは理解できる感じに解説を入れようと思っています。

また、現在コミットをしているMobbというBotフレームワークのプロジェクトに関しても、簡単な解説本を書こうと思っています。


カテゴリー: Ruby, プログラミング | コメント / トラックバック: 0個

追伸:Rubyのサンドボックスを作って、evalするBotを作った


注意:安全じゃないです

あらすじ

  • 入力されたRubyのコード文字列を安全にEvalするBotを作ったと主張する
  • 次々と安全ではないことがわかる
  • ちょっとずつ安全に向けて改良したが、まだまだ安全じゃない

詳細はここ↓

Rubyのサンドボックスを作って、evalするBotを作った

たくさん届いた指摘

前回の最後の追伸から一夜明けて、またいくつかの指摘を頂いた。それぞれに関して対策を講じていく。

refine CleanRoomできる

こういう指摘がきたので実行してみたところ、確かに壊れた。

検証のためにこういうコードを書いてみると、確かにusingしているオブジェクトの中で自身をrefineすると、すでに効いているusingが無効になるようだった。

試しにusingのなかのrefileのなかで self を見てみると、#<refinement:CleanRoom@CleanRoom> というオブジェクトが得られた。CRubyのコードを追うのは大変なのでこれがどういうものなのかがよくわからないけれど、ここに書かれた仕様を読むと、特定のスコープでrefinementという匿名オブジェクトを継承クラスに加えているだけなので、どうしてusingの内容が無効化されるのかはよくわかりません。

https://magazine.rubyist.net/articles/0041/0041-200Special-refinement.html

他にもいろいろ検証コードを書いてみた途中で思い出しましたが、RubyにはModule#ancestorsなどでは参照できない隠れたオブジェクトが存在することを、メタプログラミングRubyで読んだ気がします。オフィスに置きっぱなしで今手元にないので、後日確認して追記します。

ともあれ、対策はModule#reineを呼び出させないことで、こういう対応になりました。

https://github.com/kinoppyd/ruby-eval-bot/commit/7d67df5853c302aad168c6df94e80fd470a780d5

Sandboxモジュールの先頭でselfに名前をつけて、Moduleのrefineの中でprivate_methodsをbannned_methodにaliasしました。

ただ、このやり方一つ問題があって、どこかでbannned_methodの呼び出しが無限ループし、SystemStackErrorが発生します。どっちにしろ例外でCleanRoomの外に出るのでいいんですが、あまり健康的ではない解決策なので、無限ループの対応をする必要があります。

const_getできる

Rubyのサンドボックスを作って、evalするBotを作った

const_get(“\x45NV”)

2018/07/11 05:42


文字列のエスケープは、以前にENVのアクセスを封じてたときにすでにリスクとして認識していましたが、const_getの存在を忘れていました。なので、Moduleのメソッドへのアクセスを禁止しました。

https://github.com/kinoppyd/ruby-eval-bot/commit/3953b1b252057ffbeca4e34acfb9f5f312b0297c

TOPLEVEL_BINDINGに触れる


なるほどって感じでした、グローバル変数もかなりマズイです。

RubyのObjectに定義されたグローバル関数は、モジュール定義内で上書きすることができます。よく考えたら、ENVとかもここで書き換えておけば安全(なはず)なので、ENVの文字列チェックをやめこっちに移行しました。

https://github.com/kinoppyd/ruby-eval-bot/commit/8f0d06bc19d4d30ceca68723589391b6868604b8

気軽にSandboxということの楽しさとつらさ

社内で気軽にRubyのコードを実行できるBotが欲しくて、ものすごいマズイことが起きなければいいかなくらいの気持ちで作った実装を公開したら、思った以上の反響と邪悪な人たちと素敵な人達に反応してもらいました。もらった指摘はとても役立つもので、原因や対応策を考えるのはとても楽しかったです。

しかし、気軽なSandboxは当然気軽なものでしかなく、無限に襲ってくる脆弱性に対応するのはやっぱり大変です。ちょっと仕事の時間に遊びすぎたと反省しました。

願わくば、このエントリに追記が増えていきませんように。


カテゴリー: Ruby | コメント / トラックバック: 0個