この記事は,はてなエンジニアAdvent Calendar 2018の17日目の記事です.昨日はid:aerealさんによるTheSchwartzの失敗したjobとかerrorがいつどのように消えていくのか - Sexually Knowingでした.
今日ははてなでもよく使われているSmart::Args
のパーサを書いた話をしようと思います.
俺たちのPerl 5
はてなではPerl 5がしばしばプロダクトの開発に使われています.そしてPerlで動的な引数型チェックをできるようにするSmart::Args
というライブラリがあり,よく使われています.Smart::Args
を使うと,以下のような体裁で引数の型チェックを実行時に行うことができます.
package Dummy; use Smart::Args qw(args); # クラス定義略 sub foo { args my $class => 'ClassName', my $arg1 => 'Int', my $arg2 => { isa => 'HashRef', default => +{} }; }
このサブルーチンを呼び出すには,Dummy->foo(arg1 => 1, arg2 => +{ bar => "buzz" });
というふうに,ハッシュの形で引数をとります.
Common LispやRubyに存在するキーワード引数に似ていますね.
またハッシュではなく通常のサブルーチン同様に引数をとるためのargs_pos
もあります.これはサブルーチン定義で利用しているargs
をargs_pos
に変えることで,上述のサブルーチンをDummy->foo(1, { bar => "buzz" });
というふうに呼ぶことができるようになります.
Smart::Args
の詳細については,CPANのページをごらんください.
ちなみにこれを改良したSmart::Args::TypeTiny
をid:akiymさんが作られています.
貧弱な開発環境
さて,便利な型チェックライブラリが存在する一方で,エディタやIDEがPerlのために提供する支援はあまり充実してはいません. 他の言語には用意されているような,自動的にメソッド名や引数を補完するといった機能を提供するプラグインはほぼありません.このような状況になっているのは,柔軟な文法が静的な解析を困難にしていることに遠因があるようです.この状況はSmart::Args
でも同様で,Smart::Args
を使って定義されたサブルーチンの引数を的確にサポートできる開発環境を,わたしは存じません.
まずはパーサから
そこで今回私はSmart::Args
を使って定義されたサブルーチンの引数をサポートできるようにするための橋頭堡として,まずはSmart::Args
の文法をパースするためのライブラリを作成しました.
roswellを使ってcommon lisp処理系をインストールした状態で,ros
コマンドを使ってサンプルperl moduleファイルをパースしてみましょう.
# Roswellをセットアップする # In OS X $ sudo brew install roswell $ ros setup
$ git clone git@github.com:windymelt/smart-args-parser.git $ cd smart-args-parser $ ros -S . roswell/parse-smart-args.ros tests/Test.pm
すると,tests/Test.pm
をパースした結果が表示されます.
(("sub2" SMART-ARGS-PARSER::ARGS ((SMART-ARGS-PARSER::ONEARG (VARIABLE . "class") SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "ClassName") (SMART-ARGS-PARSER::ONEARG (VARIABLE . "arg2") SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "Int") (SMART-ARGS-PARSER::ONEARG (VARIABLE . "arg3") SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "ArrayRef"))) ("sub1" SMART-ARGS-PARSER::ARGS ((SMART-ARGS-PARSER::ONEARG (VARIABLE . "class") SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "ClassName") (SMART-ARGS-PARSER::ONEARG (VARIABLE . "arg1") SMART-ARGS-PARSER::SIMPLEARGTYPE TYPE . "Str"))))
今のところ機能はこれだけですが,これからパッケージとサブルーチンとの紐付け・Language Server Protocolの作成などを行おうと思っています.
どんな仕組みでパースしているの
当初はSmart::Args
の構文を正規表現でパースしようと思っていましたが,コーディング途中で到底これではパースできないと思いPEG
を用いたパースに置き換えました.args
やargs_pos
の構文には,型の指定・デフォルト値の指示等の目的でHashRefが登場し,これがネストする可能性があるためです.何重にもネストする構文を正規表現でパースすることは難しいのです.
PEG
args
/args_pos
の構文は,あらかじめ定義されたPEG文法ファイルをもとにパースされます.
Common Lispでは,ESRAP
ライブラリを使うことでpackrat parserを作成することができ,ESRAP-PEG
ライブラリを使うことで,PEG文法ファイルをもとに,ESRAP
を使ってPEGパーサを組み立てることができます.
このツールでもESRAP
/ESRAP-PEG
を使ってargs
/args_pos
のパーサを作成し,またパース結果を評価して「変数」や「文字列」といった意味のある単位に再編するためのコードを実装して,最終的に上掲のS式を出力させています.
実装の詳細ですが,処理の流れは以下のようになっています.
- PEG文法ファイルからpackratパーサが構築される
- ヘルパーメソッドがファイルからサブルーチン部分を抜き出す(ここは正規表現で行う)
- 抜き出されたサブルーチンの冒頭部分(セミコロンが出現するまで)を順次packratパーサに渡していく
- トークナイズとある程度の構造化が行われる
- パースされた結果をさらに適切な形に変形させる
- 必要ないトークンを削除し,文字の連続を文字列に変換する,などを行う
- 変換結果を印字する
ここがきつかったです
PEGの文法に慣れていなかったため,うっかり無限に再帰させてしまったり,パースしにくい表現にしてしまったりしました.
ここがおもしろかったです
パースに失敗するとどこで失敗したかが表示されるので,自分の入力ミスだったことがすぐに分かって面白かったです.
(esrap:parse 'smart-args-parser::hashref "{ foo -> 1 }") At { foo -> 1 } ^ (Line 1, Column 6, Position 6) In context HASHPAIR: While parsing COMMA. Expected: the character , (COMMA) While parsing FATCOMMA. Expected: the string "=>" While parsing peg-derived character class rule with clauses (32 9 10) . Problem: The production #\- does not satisfy the predicate SMART-ARGS-PARSER::|peg-derived semantic-checker for character class with clauses (32 9 10) |.
まとめ
今回は,Smart::Args
をCommon Lispでパースしたお話をしました.
明日はid:taraoさんです!