MobbのLogger
このエントリは、 Mobb/Repp Advent Calendar の十五日目です
Mobbの正規表現解釈と、MatchDataの行方
このエントリは、 Mobb/Repp Advent Calendar の十四日目です
Mobbの扱う正規表現
Mobbでは、 on/receive メソッドの引数として正規表現を渡すことが出来ます。
require 'mobb'
on /add user (\w+)/ do |name|
# name には、正規表現のキャプチャ結果が入る
end
この部分は現在、次のようなコードで解釈がされています。
def process_event(pattern, conditions, block = nil, values = [])
res = pattern.match?(@env.body)
catch(:pass) do
conditions.each { |c| throw :pass unless c.bind(self).call }
case res
when ::Mobb::Matcher::Matched
block ? block[self, *(res.matched)] : yield(self, *(res.matched))
when TrueClass
block ? block[self] : yield(self)
else
nil
end
end
end
Reppからの入力が正規表現に一致した場合、 Mobb::Matcher::Matched オブジェクトが作成され、その中のキャプチャ結果を on/receive のブロックに対して引数として渡しています。
このように、Mobbは正規表現のマッチ結果を受け取れる能力はあるのですが、あるひユーザーのひとりに「名前付きキャプチャを使いたいから、RegexpのMatchDataをそのまま触らせって欲しい」という要望を伝えられました。
そのような用途もあることは理解できるので、どうにかMatchDataをユーザーが触れるように提供してみようと思い、次のような構文を考えています。
require 'mobb'
on /(?<word>\w+) \k<word> \k<word>/ do
matched[:word]
end
このBotに対して、 “hey hey hey” と呼びかけると、 “hey” という文字列が返ってくるようにしたいとおもっています。つまり、 on/receive の引数に正規表現をとった場合、matchedというアクセサがブロックの中で利用でき、呼び出すとRegexp#matchの戻り値が得られるような構文を考えています。
これらは、Sinatraでいうところの request/response/params といったアクセサと同じ扱いになりますが、SinatraがRackからの呼び出しの直後にこれらの変数を初期化するのに対し、Mobbでは初期化のタイミングがすこし遅くなることが変更点となるでしょう。
機能追加のリクエストお待ちしてます
BotはBotと会話するべきかどうか?
このエントリは、 Mobb/Repp Advent Calendar の十三日目です
Mobbのメソッド呼び出しをチェーンする、 chain/trigger シンタックス
このエントリは、 Mobb/Repp Advent Calendar の十二日目です
Mobbにできないこと
これは、Mobb/Reppの未来の実装の話です。
Mobb(というよりも、Repp)の最大の特徴に、「発言を受け取り、返答を戻り値で返す」というシンプルな記述があります。これはとても強力でシンプルなルールですが、その一方でいくつか困ったことも起こります。
たとえば、Botにあるとても時間のかかる作業がさせたいとします。何も考えずに実装すると、その作業の起動トリガーを発言し、Mobbがそれを受け取り作業をし、完了したという文字列を返します。これは一見何もおかしなことは無いように見えますが、一つ大きな問題を抱えています。それは、時間のかかる作業の間、Botは無言で作業をこなしていて、その作業を起動させた人は、Botがちゃんと仕事をしているのか、それとも無言で死んだのかを区別する方法がありません。
次に、少し人間に優しい実装をしてみましょう。無言で死ぬのではなく、何かしらのエラーが発生した場合は、それを戻り値としてサービスに返せば、少なくとも何かエラーが発生したことはわかります。エラーメッセージの返し方によっては、何が原因で失敗したのかという細かい情報も得られるでしょう。これで概ね困ることは無いように思いますが、本当にそうでしょうか? この実装では、Botが作業を開始する指示を正しく受け取ってくれたのかどうかがわからないという問題が残っていませんか?
最後に、完全に人に優しい実装を考えてみましょう。作業の起動トリガーとなる発言を受けると、Botは即座に「作業を開始します」と返答します。その後、時間のかかる作業をやりながら、適宜「いまXXまで作業が終わりました」と教えてくれれば、ものすごく優しくないでしょうか? 少なくとも、コマンドを間違えてBotが起動してくれなかった場合などにはすぐ異常を検知できますし、何よりBotに愛着がわきます。
ところが、現状のMobb/Reppの構成では、このような適宜返事を返すBotと言うものを作ることが出来ません。なぜなら、最初に書いたとおり、Mobb/Reppはメソッドの戻り値がサービスの発言となり、途中経過で何かしらのアクションをすることが出来ないからです。そして、Reppはインターフェイスというその思想から、Mobb側にサービスに対してアクションをする方法は完全に秘匿されており、何もすることはできません。これは、よくあるBotフレームワークが、処理の途中で適宜サービスへのアクセスができることと大きな違いです。
しかし、Mobb/Reppは秒でクソボットを作れるフレームワークであると同時に、快適なBotを人々に提供することも軽視しているわけではありません。それでは、この問題を解決するにはどうすればいいでしょうか?
chain/trigger
Rubyを使って秒でBotを作るなら、秒でRedisだって使えなきゃ、やっぱり話にならないですよね?
このエントリは、 Mobb/Repp Advent Calendar の十一日目です
mobb-redis
昨日のエントリでは、MobbでActiveRecordを利用する方法を紹介して、Sinatraの資産をMobbが継承できるという話をしました。そして今日はRedisです。こっちは本当に秒でいけます。
https://github.com/kinoppyd/mobb-redis
まず、検証用のRedisを立てましょう。今ならDockerで秒です。
docker pull redis
docker run --rm -p 6379:6379 redis
次に、mobb-redisをインストールします。例ではBundlerを使います。
# frozen_string_literal: true
source "https://rubygems.org"
gem "mobb"
gem "mobb-redis"
最後にアプリケーションを書きます。
require 'mobb'
require 'mobb-redis'
register Mobb::Cache
on 'hello' do
settings.cache.fetch('great') { "hello world #{Time.now}" }
end
はい、秒ですね。起動して動作を見てみましょう。
bundle exec ruby app.rb
== Mobb (v0.4.0) is in da house with Shell. Make some noise!
hello
hello world 2018-12-11 01:36:16 +0900
hello
hello world 2018-12-11 01:36:16 +0900
このように、返答の中の時間が変化していないので、Redisを経由していることがわかります。
redis-sinatra
Rubyを使って秒でBotを作るなら、秒でActiveRecord使えなきゃ話にならないですよね?
このエントリは、 Mobb/Repp Advent Calendar 2018 の十日目です
ActiveRecord
ここ数日書いているエントリで、Mobbがいかに秒でBotを作れるすごいやつかというのは伝わったかと思います。しかし、世の中には秒で複雑で素敵なBotのアイディアが思い浮かぶ人もいます。そして、そういう人の多くは「いやいや、いくら秒でロジック書けても、DB使えないと話にならんでしょ」という人もいます。
そうです、Botのロジックは会話的なものが多いため、状態を記録したりデータを保持したりという行動を行いたいケースが非常に多いです。それでは、DBを使いましょう。RubyでDBといえば、ActiveRecordですね。ActiveRecord、使いたくないですか?
Mobbは、秒でBotを作るエンジニアを本気で応援するフレームワークです。当然、ActiveRecordも使えなくては話になりません。
安心してください、使えますActiveRecord。たった一つのgemを追加するだけで。
https://github.com/kinoppyd/mobb-activerecord
Usage
Mobb, test, spec
このエントリは、 Mobb/Repp Advent Calender の八日目です
Reppのインターフェイス
この記事は Mobb/Repp Advent Calendar の7日目です
Reppのインターフェイス
少し前のエントリで、Reppはインターフェイスだと解説しましたが、実はそのインターフェイスの定義をきちんと書いたドキュメントは存在しません。それは何故かと言うと、まだインターフェイスをどのように固めれば柔軟に今後対応していけるかという確信が、書いている本人である私にないからです。現状の仮実装として、Slackを事実上の対象サービスとして想定した記述ができるように、次のように実装されています。
-
environmentのハッシュを引数に一つ受け取るcallメソッドが実装されている
-
2つの要素からなる配列を返す。サービスに投稿する文字列が1つ目の要素、各サービスごとにオプショナルに解釈してほしい内容をハッシュで2つめの要素に入れる
ReppはRackの模倣をして作られた、各種サービスとBotフレームワークとの間をつなぐ取り決めですが、Rackの取り扱うHTTP(正確にはWebサーバー)の世界とは違い、Botが接続するSlackやShellやChatWorkやLINEや、あるいはHTTPかもしれないサービスとの接続はその形式が多種多様に渡り、抽象化するとどのようにまとまるのかをうまく調整することが難しいのです。
たとえば、SlackにはAttachmentsというチャットの発言とは別にチャットの内容を装飾するための取り決めがありますが、それは他のサービスにはありません。しかし、Reppとしては存在したりしなかったりする内容に関してもきちんと橋渡しをする必要があります。
Mobbの実装とSinatraの実装の比較
このエントリは、Mobb/Repp Advent Calendar の六日目です
Mobbの実装の元ネタ
これまで何度も出てきましたが、Mobbの実装はSinatraから非常に強烈な影響を受けています。しかし、はっきりと宣言しておきますが、強烈な影響を受けているなんていう生易しいものではありません。Mobbのコードは、ほとんどSinatraをコピペして作られていると言っても過言ではありません。
Mobbの文法やDLSのIFは、ほぼSinatraの形を踏襲しています。そしてその形を受け継ぐのであれば、どうやってもSinatraと同じコードが頻出することが最適解になっていきます。なにせSinatraは10年以上の歴史があるフレームワークで、その歴史の中でコードが無駄なく洗練され続け、その後追いをするならば必然的に同じコードが頻出することになります。なぜなら、Sinatraはわずか2000行ほどのコードで構成され、どうしてもそれ以上コードを削ることが難しいためです。
Sinatraと全く同じコード
昨日のエントリでも書きましたが、helpers/registerは全く同じコードが出現します。
def helpers(*extensions, &block)
class_eval(&block) if block_given?
include(*extensions) if extensions.any?
end
def register(*extensions, &block)
extensions << Module.new(&block) if block_given?
@extensions += extensions
extensions.each do |extension|
extend extension
extension.registered(self) if extension.respond_to?(:registered)
end
end
この2つは本当に全く同じコードを使っています。そのおかげで、Sinatraの資産をそのまま流用できる箇所でもあります。例えば、mobb-activerecordなどです。
他にも、before/afterフィルタや、invokeメソッド、デバッグ用にスタックトレースを追跡するコードや、デリゲータのコードなど、全く同じものが出てくる箇所がいくつもあります。
Mobbの機能拡張を実現するhelpersとregister
この記事は Mobb/Repp Advent Calendar の五日目です
Mobbの機能拡張
これまで見てきたように、MobbにはSinatraとほぼ同じ仕組みが多数存在します。そして今日紹介するhelpers/registerメソッドも、Sinatra由来の機能です。これら2つのメソッドは、Sinatraと全く同じコードが書かれているので、挙動も全く同じです。
helpers
helpersメソッドは、トップレベルモード(require ‘mobb’をしてそのままロジックを書き始めるケース)では暗黙的に作成されるMobbのアプリケーションクラスを、モジュラーモード(require ‘mobb/base’ して自分でクラスを定義するケース)ではhelpersが呼び出されたクラスをそれぞれコンテキストとして、渡されたブロックをclass_evalで実行します。ブロックではなくモジュールを渡した場合は、そのモジュールがincludeされます。
require 'mobb'
def hoge
'hoge'
end
on 'hello' do
hoge
end
require 'mobb/base'
class Bot < Mobb::Base
helpers do
def hoge
'hoge'
end
end
on 'hello' do
hoge
end
end
Bot.run!
モジュラーモードでは、そもそもhelpersの中で定義しようがクラスの中で定義しようが、クラスのインスタンスメソッドとして定義されるのであまり違いはありません。ですが、トップレベルモードの場合、Rubyのmainクラスで定義したメソッドはObjectのプライベートメソッドとして定義されるので、いろいろなものを汚染しかねません。そのため、暗黙的に作られるMobbアプリケーションのクラスに、helpersを使って直接定義を行います。
https://docs.ruby-lang.org/ja/latest/class/main.html
やっていることは結局の所include呼び出しとあまり変わりませんが、ブロックを渡せるところが軽量でいいと思います。
helpersメソッドの中身は、Mobb::Baseを継承したアプリケーションを拡張しています。