morimorihogeです。引きこもってると生活リズムが崩壊してきますね。平日は数分の朝会をやっているのでそこで一応一日一回は強制的に時刻同期されていますが、これがないと本格的に日本時間で動かなくなるかもしれない。
少々間が空いてしまいましたが2020/05/15(金)にZoom開催された銀座Rails#21 @リンクアンドモチベーションでTechRachoの週刊Railsウォッチの出張版ということで参加させていただきました。スライドは以下の通りです。
今回の特集は社内の勉強会で以前話したことのあるテーマから、ABNFの話を引っ張ってきました。Railsと直接は関係ないですが、開発一般の話として知らない人はぜひどうぞ。
標準仕様について
我々がインターネットで通信をしたり、Webで色々なサービスを利用したりするとき、その背景には様々な標準仕様があり、そこで規定されたプロトコル(通信規約)に従って通信をしています。
インターネット界隈ではIETFの運用するRFC、HTMLの仕様なら今はWHATWGなどがありますね。通信の下の方のレイヤに行くとIEEEなんかもありますが、こちらはL2とかその下のイメージがあるので、Webエンジニアというよりはインフラエンジニアの方が気にすることが多いかもしれません。
何かソフトウェアを作って相互通信しようと思ったときに、開発者が毎度独自仕様で仕様を決めてしまうと、いざ他のソフトウェアやハードウェアと通信する際に相互接続が大変になってしまいます。
独自ソフトウェアであれば別に独自仕様で問題ないじゃないか、と思う人もいるかもしれませんが、既存の標準仕様に則って作られていることで
- 仕様設計段階での工数が大幅に削減できる
- 仮に実装したものが将来保守できなくなっても、何らかの方法で別のソフトウェアに移行することがしやすくなる
- 一般に知られたプロトコルであれば、当該ソフトウェアの開発・保守を行う際に事前知識を得たソフトウェアエンジニアを確保しやすい
などの利点があります。
標準仕様は当該プロトコルを使う企業同士のデファクトスタンダード取り戦争のような側面もありますが、それが故に内容は世界中の専門家からレビューされています。
また、標準仕様はあくまで「仕様」を規定するだけで「実装」については規定されていませんが、大抵の場合仕様がリリースされる頃にはリファレンス実装も既に世の中にあることが多いため、絵にかいた餅ということもほとんどありません。
※これは標準化戦争の戦略として、各ベンダが自分のプロダクトにある新機能を標準仕様にしていくことが多いためです(GoogleのSPDY -> HTTP/2などはまさにこの典型)
アプリケーション開発者と標準仕様
それでは、Railsアプリケーション開発者などのWebアプリのアプリケーション開発者が標準仕様を意識するのはどんな時があるでしょうか?
まず前提として、Railsは新しいものから古いものまで、様々な標準仕様にかなり準拠できるように作られています。
軽くActiveSupportを眺めてみるだけでもTime.rfc2822
といったRFC準拠書式に変換するメソッドが用意されていたりしますし、ActionDispatch::SSLのSSLリダイレクトでリダイレクトされる際、GETメソッド以外の場合には 301 Moved Parmanently
ではなく 308 Parmanent Redirect
(RFC 7538) を使うようになったりなど、普通にアップデートされたRailsをRails Wayで実装していれば、自ずと標準仕様に概ね準拠した開発ができる点が多いかと思います。
一方で、フレームワークではなくアプリケーション自身で解決しないといけないような部分の仕様についてはRails本体には搭載されていないので、OAuth2認証なんかはGemの助けを借りる必要があったりします。
こうしたRailsに実装されていないものを実装する際は、アプリケーション開発者が標準仕様を(あれば)意識して実装する方が良い設計になりやすいでしょう。
他にも、外部サービスとのAPI連携がある場合、接続点での障害は接続元・接続先のどちらが問題なのか問題になりがちですが、標準仕様があることで「こちらは標準仕様に則って接続しているのを確認している」という答え方ができるようになります。
アクティブなOSSでない企業の独自ソフトウェアなどは、たまに標準仕様通りの通信を受け入れてくれないことがあるため、そうした場合にどちらに否があるのかを明らかにするためには標準仕様を調べて読める必要があるわけですね。
※そんなことあるのか?と思う人もいるかもしれませんが、僕は15年くらいのエンジニア歴の中で数度通信先側の標準規約違反でトラブった経験があります。全てを疑え
ABNF(Augmented Backus-Naur Form)
さてここでようやくABNFの話になります。
RFCなどを読んでいると気が付くのが、データ形式などの解説で出てくる見慣れない言語です。以下みたいなやつですね。
# メールアドレスフォーマットに関する標準仕様
# https://tools.ietf.org/html/rfc5322 より引用
addr-spec = local-part "@" domain
local-part = dot-atom / quoted-string / obs-local-part
domain = dot-atom / domain-literal / obs-domain
domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
dtext = %d33-90 / ; Printable US-ASCII
%d94-126 / ; characters not including
obs-dtext ; "[", "]", or "\"
これは文脈自由文法を定義するためのメタ言語としてメジャーなBNF(Backus-Naur Form)の亜種でABNF(Augmented Backus-Naur Form)という言語です。
文脈自由文法とは何かみたいな話に興味がある人はオートマトンに関する書籍や資料を当たってみるといいかもしれません。情報系の大学なんかだと学部で履修することもあるかも?(僕は学部で習いました)
ちなみにABNF自身もRFCとして定義されており、定義を読むことができます。そんなに大きな仕様ではないので興味があればどうぞ。
さて、ABNFのような言語を使うと文法そのものを定義して記述することができます。
具体的なプログラミングとしての使い方としては、ある入力が与えられたときにその入力が文法に沿っているかどうかを判定する使い方(いわゆるparse errorの判定)だったり、Yaccのような構文解析機ジェネレータ(いわゆるコンパイラコンパイラ)に食わせてコンパイラの一部として利用したりといった用途がメインでしょうか。
ただ、ここではそうしたコンパイラやパーサーとしての利用ではなく、人間でも読める文法規則であるということが大事です。
ある文法について議論するときに、典型的な方法としてはサンプルをいくつか出して議論することになると思いますが、その方法ではどうしても漏れが出てしまうことがあり、曖昧さから認識齟齬が出てしまいます。
標準仕様において「正しい解釈が複数あり、それらが競合する」という状況は悪夢なのでそれを避けるためにもABNFのようなきちんと文法を文法として定義できる言語を利用するわけですね。
ABNFの基本をRFC3339 timestampで見てみる
ABNFの基本はこれだけです。
rule = definition ; comment CR LF
左辺に定義したいルール名を書き、右辺に定義したいルールを書きます。末尾にセミコロンを書き改行(CR LF)すると次の定義に移ります。
また、右辺のdefinitionではそれよりも前に定義したruleを利用して構いません。
この辺りを踏まえてRFC3339 Date and Time on the Internet: Timestampsを追いかけていきましょう。
RFC3339概要
RFC3339はインターネット上で使われるタイムスタンプ文字列を仕様化したものです。
たかがタイムスタンプというなかれ、何もないところから安全なタイムスタンプを定義しようとすると実はかなり考慮しないといけないことが多いことが分かります。UTC/JSTなどのタイムゾーンの考慮、うるう秒、サマータイムなど「あり得る」タイムスタンプをすべて考慮するのは難しいものです。
参考までに、以下の形式は全てRFC3339準拠です。
1985-04-12T23:20:50.52Z
1996-12-19T16:39:57-08:00
1990-12-31T15:59:60-08:00
※秒の部分に60秒が許容されている(うるう秒)1937-01-01T12:00:27.87+00:20
Rubyでは、DateTime#rfc3339辺りを使えば簡単にRFC3339準拠の文字列を得ることができますね。
ちなみにRailsであればActiveSupportのTimeWithZoneの中で、#xmlschema という関数に統合されていたりします
定義の確認と分解
さて、RFC3339の定義部分(5.6. Internet Date/Time Format)から定義部分を引用します。
date-fullyear = 4DIGIT
date-month = 2DIGIT ; 01-12
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
; month/year
time-hour = 2DIGIT ; 00-23
time-minute = 2DIGIT ; 00-59
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
; rules
time-secfrac = "." 1*DIGIT
time-numoffset = ("+" / "-") time-hour ":" time-minute
time-offset = "Z" / time-numoffset
partial-time = time-hour ":" time-minute ":" time-second
[time-secfrac]
full-date = date-fullyear "-" date-month "-" date-mday
full-time = partial-time time-offset
date-time = full-date "T" full-time
ここで大事なのは一番最後の
date-time = full-date "T" full-time
です。手前にあるすべてはこのdate-time
の文法を定義するために使われる要素なわけです。
こんな感じで、ABNFではその仕様上後ろに行くほど抽象度の高い定義が記述されることが多いというのは知っておくと読みやすくなるでしょう。
それではこのdate-time
定義を後ろから前に向かって辿っていきます。
これは私見ですが、ABNFを読む際、ボトムアップ式に詳細な定義を確認したい場合は上の行から読めば良いですが、トップダウン式に全体概要から把握したいときには下の行から読み進めていく方が全容を把握しやすいです。
「知らない定義を見つけたらその定義元に飛んで、その定義を読む」という芋づる方式で読み進めていくと、イメージとしては以下のような感じになります。
date-time
-> full-date
-> date-fullyear
-> 4DIGIT
まで来ました。4DIGIT
の定義はないのでこれ以上は深く定義を終えません。
ここで出てくる4DIGIT
はいわゆる終端記号(Terminal Values)というもので、ABNFによって仕様として定義された記号になります。ABNFによって DIGIT
は「0-9の数字文字」、手前に書かれた4は「4回連続で出現する」ことを表すので4DIGIT
は「4桁の数字」になるわけですね。
※1: 余談ですが、ここを見るだけでもRFC3339の西暦は4桁で省略してはいけないことが分かりますね。3桁の年号を表現する場合には794
ではなく0794
としないと4DIGIT
を満たさなくなるのでRFC違反となります。同様に紀元前も
※2: 5桁の西暦は仕様上表現できないので、人類はきっとそれまでには滅びるか新しいtimestamp仕様を作り出すのでしょう
※3: 同様に紀元前は表現できないように見えますが、RFC3339の元になったISO8601では紀元前も表現できるようです。紀元前を扱いたいアプリケーションではRFC3339を使ってはうまくいかなさそうですね(学び)
余談を書いてたら話がそれました。ABNFをトップダウンで読んでいくときは、上記のように全ての定義を終端記号まで分解することが目標となります。
実際にやってみましょう。以下ではfull-date
を終端記号まで展開しています。
full-date = 4DIGIT "-" 2DIGIT "-" 2DIGIT
となりました。なるほど。いわゆるYYYY-MM-DD
な日付書式そのものですね。
同様に後半のfull-time
も展開していきます。
はい、展開できました。
date-time = 4DIGIT "-" 2DIGIT "-" 2DIGIT
"T"
2DIGIT ":" 2DIGIT ":" 2DIGIT
["." 1*DIGIT] ("Z" / (("+" / "-") 2DIGIT ":" 2DIGIT))
が最終的な定義です。[]
で囲った部分はoptional(あってもなくても良い)、()
は集合をグループ化、/
はORに相当します。サンプルとも見比べてみるとイメージが掴めるのではないでしょうか。参考までにもう一度貼っておきます。
1985-04-12T23:20:50.52Z
1996-12-19T16:39:57-08:00
1990-12-31T15:59:60-08:00
1937-01-01T12:00:27.87+00:20
定義展開の際の注意
さて、ABNFは終端記号まで展開すると、読みにくくはあれどルールとして明確なものになるような話をしてきましたが、ここで注意があります。
RFCに書かれるABNF定義はあくまで実装ではなく仕様を人間に説明することが目的なので、終端記号まで展開したものだけを見ると意図と違う読み方をしてしまう危険性があります。
full-date
の例で見てみましょう。
最終的にfull-date
は4DIGIT "-" 2DIGIT "-" 2DIGIT
に展開されましたが、実際にはこの2DIGIT
にはコメントが付いていました。
date-month
は01-12、date-mday
は01-28, 01-29, 01-30, 01-31 based on month/year`とありますね。
コンピュータに計算させるプログラミング言語ではコメント部分というのは動作させる際には無視して構わないものですが、人間に読ませるABNFではこのようにコメント部分に大事な制約条件が書かれていることがあります。
「いや、そういうルールがあるならそれも式にせいや」という人がいるかもしれませんが、標準仕様はあくまで仕様を定義するものであり、実装を決めるものではないのでこれくらいの抽象度合いが妥当でしょう。
全てを詳細仕様として記述してしまうと本質ではない部分のドキュメントが大量になってしまい、ドキュメントの品質が損なわれてしまいます。
ちなみに標準仕様で定義し切らなかった部分の解釈の揺れによる仕様不整合が発生することも時にはありますが、そうした場合はどちらが悪いというわけでもなくすり合わせをしたり、標準仕様の改定が行われたりします。
もっとABNFに触れたい人へ
軽いものだとRFC4648のBase64/32/16 Encodingなんかが練習に良いと思います。シンプルなので迷わずいけます。
もうちょっと難易度&実用性を上げるならRFC3986のURI:Uniform Resource Identifierは結構歯ごたえがありますが、URLの各部分名称(scheme, authority, path, query, fragment)を意識して覚えることができるので良い教材かもしれません。
RFC以外も見たいということならW3CのContent Security Policyなどに登場しています。これに限らず文字列で相互通信するプロトコルについてはABNFがよく使われますので、出てきたときに読めるようにしておくことは無駄ではないでしょう。
まとめ
銀座RailsというRailsの勉強会なのにABNFというRails全然関係ない話をしてしまいましたが、ABNFの読み方なんかは業務で学習する機会がない割に知っておくとたまに助けられるものなので、これを機に知らなかった人は触れてみても良いのではないかと思います。
その他、プログラマ同士が喧嘩せず明確な仕様のやり取りをする上でもABNFは有用ですので、何か独自のメッセージングプロトコルを実装して相互通信するようなものを実装する際には仕様として記述を検討してみてもいいかもしれませんね。
ではでは。