pockestrap

Programmer's memo

RubotyでSKIコンビネータ計算する

RubotyはRubyで書かれたチャットボットフレームワークです。

github.com

RubotyはRubyプラグインを実装して拡張できます。つまりRubyでできることはRubotyでできます。便利ですね。

ということでRubotyでSKIコンビネータ計算をしてみました。 しかし、Rubyプラグインを書いたら簡単に実装できてしまってつまらないですね。1 そのため今回は、自分ではプラグインを書かずにSKIコンビネータ計算を実装しました。

SKIコンビネータ計算とは

ja.wikipedia.org

私も今回初めてSKIコンビネータ計算について調べました。ちゃんとした説明ができる気がしないので、Wikipediaなりなんなりを読むと良いと思います。

使用するプラグイン

今回はプラグインの実装はしませんが、既存のプラグインを2つ使います。ruboty-replaceとruboty-echoです。

ruboty-replace

github.com

ruboty-replaceは、受け取った発言を正規表現で置換して、それを発言として解釈するプラグインです。 次のように使えます。

# https://github.com/makimoto/ruboty-rainfall_jp を使う例です

# replaceする前
# コマンドが少し長い
> ruboty tell me rainfall at Kyoto
Rainfall forecast: Kyoto (135.75141900,35.01042850)
07-18 22:15 0.0 mm/h
07-18 22:25 0.0 mm/h
07-18 22:35 0.0 mm/h
07-18 22:45 0.0 mm/h
07-18 22:55 0.0 mm/h
07-18 23:05 0.0 mm/h
07-18 23:15 0.0 mm/h

# replaceをする
> ruboty replace tenki with tell me rainfall at Kyoto
Registered

# replace後
# replace前と同じコマンドが実行される
> ruboty tenki

なお、今回の例だとruboty image tenkiのようなコマンドも同様に置換されてしまうので、使う際には少し注意が必要です。

ruboty-echo

github.com

こちらはとても単純なプラグインで、Rubotyに発言をさせられます。

次の例のように、ruboty echoに続く文字列をrubotyが喋ります。

> ruboty echo hello
hello

f:id:Pocke:20200130012924p:plain
Slackで見るとこんな感じ

これだけだと単純ですが、RubotyはRuboty自身の発言もトリガーとすることができます。2 つまり、次のようなことができます。

> ruboty echo ruboty echo hello
ruboty echo hello
hello

f:id:Pocke:20200130013327p:plain
Slackでみるとこんな感じ その2

この例では、Rubotyに「ruboty echo hello」と言えと命令し、そしてRubotyが自分自身に「hello」と言えと命令し、その結果「hello」が出力されています。

ruboty-replace と ruboty-echo の組み合わせ

そして、この2つのプラグインを組み合わせることでループを作れます。たとえば無限ループを作るには次のようにすると良いでしょう。

> ruboty replace ruboty loop with ruboty echo ruboty loop
Registered

> ruboty loop
ruboty loop
ruboty loop
ruboty loop

とても便利そうな気がしてきましたね。

ruboty-replaceは与えられた文字列をgsub!するので、gsub!whileだけでRubyでSKIコンビネータ計算を実装すれば、それをそのままRubotyに移植できそうです。

SKIコンビネータ計算の実装

さて、今回使う道具は揃いました。 この2つのプラグインを使えばSKIコンビネータ計算を実装できます。

今回は次のような方針で実装しました。

  • ruboty ski <式>のようなコマンドを、各法則に従って書き換えていく正規表現をruboty-replaceで登録する
  • 各法則を1つずつ適用した後、まだ計算を適用できればruboty echo ruboty ski <式>する
  • これ以上計算できなければruboty echo <式>をして計算結果を表示して終了する

そして、実際にruboty-replaceに登録した命令は次になります。

ruboty replace ^ruboty ski (?<simple>(?:[()]|[^ski])*)s(?<expr1>[^()]|\(\g<expr1>+\))(?<expr2>[^()]|\(\g<expr2>+\))(?<expr3>[^()]|\(\g<expr3>+\))(?<rest>.*) with ruboty ski \k<simple>(\k<expr1>\k<expr3>)(\k<expr2>\k<expr3>)\k<rest>
ruboty replace ^ruboty ski (?<simple>(?:[()]|[^ski])*)k(?<expr1>[^()]|\(\g<expr1>+\))(?<expr2>[^()]|\(\g<expr2>+\))(?<rest>.*)$ with ruboty ski \k<simple>\k<expr1>\k<rest>
ruboty replace ^ruboty ski (?<simple>(?:[()]|[^ski])*)\((?<content>(?<expr1>[^()]|\(\g<expr1>+\))*)\)(?<rest>.*) with ruboty ski \k<simple>\k<content>\k<rest>
ruboty replace ^ruboty ski (?<simple>(?:[()]|[^ski])*)i(?<rest>.+) with ruboty ski \k<simple>\k<rest>
ruboty replace ^ruboty ski (?<simple>(?:[()]|[^ski])*)$ with ruboty echo \k<simple>
ruboty replace ^ruboty ski (?<content>.*[ski].*)$ with ruboty echo ruboty ski \k<content>

これを登録すればお使いのチャットサービスでSKIコンビネータ計算ができて便利…! なのですが、「まだ計算を適用できれば〜」の条件をとても雑に書いているため、注意が必要です。3 具体的にはruboty ski sなどと発言するだけで容易に無限ループします。 お試しの際はどうか自己責任でお願いいたします。

実装の説明

実はこの正規表現は手書きしたのではなく、次のRubyスクリプトを書いて生成しました。

expr_gen = -> (n) { "(?<expr#{n}>[^()]|\\(\\g<expr#{n}>+\\))" }
simple = '(?<simple>(?:[()]|[^ski])*)'

S = /^#{simple}s#{expr_gen.(1)}#{expr_gen.(2)}#{expr_gen.(3)}(?<rest>.*)/
K = /^#{simple}k#{expr_gen.(1)}#{expr_gen.(2)}(?<rest>.*)$/
I = /^#{simple}i(?<rest>.+)/
P = /^#{simple}\((?<content>#{expr_gen.(1)}*)\)(?<rest>.*)/

また、Slackに登録する前に次のコードで動作を確認しながら実装していました。 https://gist.github.com/pocke/48417a4b119c84520f5176fe15f19667

expr_gen

まず、expr_genは"式"にマッチする正規表現として使う文字列を生成する関数です(なんとなく「式」と言っているけど、これをなんて呼ぶべきなのかはよくわからない)。 式は「カッコではない文字1文字」もしくは「カッコの中に1つ以上の式が連続している文字列」を指します。 つまり、x(xy)((xy))(x(y))は式です。 一方xx()(()は式ではありません。

なお文字列を生成するようにしているのは、1つの正規表現内で複数回式を使う時に、キャプチャのための名前が被らないようにするためです。 expr_gen.(1)のように呼び出すと、expr1という名前でキャプチャを作るようになっています。

ところで、「おや?」と思った方がいるかも知れません。 この正規表現は明らかにカッコの対応を取る必要がありますが、普通正規表現はカッコの対応を検出できません。 ですが、Ruby正規表現では「部分式呼び出し」という機能を使うことでカッコの対応を検出できます。 詳しくは https://docs.ruby-lang.org/ja/latest/doc/spec=2fregexp.html#subexp を参考にしてください。

simple

simpleは、「これ以上変換できない文字列」にマッチする正規表現として使う文字列を生成する関数です。 そのような文字列は単に読み飛ばして、後続の文字列を評価するようにしています。

S, K, I

そのままSKIコンビネータ計算の各関数にマッチする正規表現です。

P

これは、余分なカッコを外すための正規表現です。

SKIコンビネータ計算の実行

これを実際にSlackに登録して実行してみると、次のようになります。 今回はWikipediaに載っている「式の逆転」の例を試してみました。

f:id:Pocke:20200130020428p:plain
Slackに登録する様子

f:id:Pocke:20200130020448p:plain
計算を実行する様子

計算が動いていて面白いですね。


みなさんもRubotyやSKIコンビネータ計算で遊んでみてはいかがでしょうか。


  1. まあ、それはそれで面白いとは思いますが。

  2. SlackでRubotyを動かした場合には自身の発言に反応しますが、他のチャットサービスだとどうなるのかはわかりません(CLIでは自身の発言には反応しませんでした)

  3. たとえ雑じゃなくても無限ループするように書けば無限ループするのでそれはそうという感じですが