基本は DNS に TXT レコードを足すだけですが、自ドメイン宛のメールを Gmail に転送 (forward) している場合は一工夫が必要に思います。例えば自ドメインが @foo.com だっとして、[email protected] 宛のメールを [email protected] に転送するようになっていたとします。
送信者 [email protected] から [email protected] へメールを送信すると、foo.com の smtp サーバがメールを受信した後、Gmail へメールを再送信します。
このとき Gmail から見るとメールの送信者は bar.org なのに smtp サーバは foo.com で bar.org からのメールが bar.org の SPF レコードに含まれないサーバから送信されているので SPF 認証に失敗してしまいます。
ARC を設定すればそれでも問題ないのかもしれませんが、世の中には envelope from を強制的に書き換えて場当たり的に対処する work around が知られているようです。 今回はとりあえず、それで様子をみることにしました。
転送時に envelope from(具体的には Return-Path:)を強制的に [email protected] に変えるためには、まず /etc/postfix/main.cf に以下を追加します。
sender_canonical_classes = envelope_sender
sender_canonical_maps = regexp:/etc/postfix/sender_canonical
次に /etc/postfix/sender_canonical を次のような内容で作成します。
/@foo.com$/ @foo.com
/^[^@]+@([a-z0-9_+\-\.]+)$/ [email protected]
この設定で、自ドメインである foo.com から直接送信されている場合は envelope from を変更せず、そうでないときは強制的に [email protected] に変わるはずです。
[email protected] は smtp サーバからエラーメールを受け取れるアドレスであれば何であっても問題ありません。
こうしておいて、
sudo postmap /etc/postfix/sender_canonical
sudo systemctl restart postfix
とすれば設定が反映されるはずです。
ただ、原因は調べ切れていませんが、このやり方だと、Gmail から [email protected] へ送信された場合、転送された先の [email protected] では SPF 認証に失敗するようです。
Gmail 側で受信したメールを見てみると Return-Path: が [email protected] に変わっていません。
この問題を避けるためには .forward や /etc/aliases の設定で
me: "|/usr/sbin/sendmail -oi -f [email protected] [email protected]"
のようにすればよいことを発見しました。
この設定が必要なのは Gmail に転送しているアカウントだけです。
それ以外の場合は sender_canonical_classes の設定だけで対応できているようです。
こちらは素直に設定すれば良いようです。
sudo apt install opendkim opendkim-tools
sudo opendkim-genkey -D /etc/dkimkeys -d foo.com
sudo chown opendkim:opendkim /etc/dkimkeys
sudo chown opendkim:opendkim /etc/dkimkeys/default.txt
sudo chown opendkim:opendkim /etc/dkimkeys/default.private
その後、/etc/opendkim.conf に次のような行を追加、あるいは変更します。
Domain foo.com
KeyFile /etc/dkimkeys/default.private
Selector default
Socket inet:8891@localhost
Mode sv
そして公開鍵 /etc/dkimkeys/default.txt の内容を DNS に登録します。
このファイルの中身をそのまま使うことはできないので、手で適当に整形する必要があります。
その後 /etc/postfix/main.cf に以下を追加します。
smtpd_milters = inet:127.0.0.1:8891
non_smtpd_milters = $smtpd_milters
milter_default_action = accept
そして
sudo systemctl restart opendkim
sudo systemctl restart postfix
このように DKIM の新しい設定を反映させれば完了です。
以上です。メモですので何か間違いがあってもご容赦ください。
]]>(株)Sider 千葉 滋
リファクタリングはしなくてもよい、というのは少し極端すぎるタイトルで、 一般論を言えばリファクタリングはした方が良いでしょう。 一方で、リファクタリングは時間がかかるので開発が停滞するとか、 動いているプログラムを動かなくする危険があるとか、負の側面も知られています。
本稿ではリファクタリングをしなくてもよい理由を書くのですが、 リファクタリングには負の側面があるからしなくてよい、 という視点ではなく、リファクタリングをしなくても、いわゆる技術的負債を軽くする方法はある、という視点で書いてみようと思います。 将来、こういう方向の技術が発展するかも、という少し未来予測じみた話です。
最初に背景知識として、リファクタリングの難しさについて書きます。 これは良く知られた話なので、ご存じの方は読み飛ばしてこちらの結論だけどうぞ。
リファクタリングといっても幅広い話です。以下では重複コードを取り除くリファクタリングに絞って話を進めることにします。
重複コードとは、文字通り、プログラムの中に2回以上現れる同じような字面の(しかし完全に同じとは限らない)コード断片のことです。 コードクローンとも言います。 プログラムを書いているときは、似たような処理だからといって、プログラムの一部をコピーして別のところにペーストし、少し直す、という作業をやりがちです。 このような「コピペ」をすると重複コードが知らぬ間に生まれていきます。
重複コードがあると、そこにバグがあった場合、全てのコピーを漏れなく直さなければなりません。 同じようなコードが何カ所にも散らばっているわけです。 一カ所だけバグを修正して、それで直したつもりが、他の場所を直し忘れていて、 いっこうにバグが減らない、ということになりがちです。 こういうことがあるので、重複コードの量はよくコード品質を計る指標にも使われます。
プログラムの中に重複コードがあっても、適切にリファクタリングすれば重複コードを取り除くことができます。 重複部分を関数やメソッドとしてくくりだして、それぞれのコードから呼び出すようにすればよいのです。
例えば図1のようにプログラムの2カ所に重複コードがあるとします。 この部分を図2のように関数やメソッドにくくりだして、それぞれ元の場所から呼び出すようにすれば重複コードを除去することができます。


もっとも重複コードを除去できるといっても、除去後のプログラムが必ず読みやすい良いものになるとは限りません。 重複コードは良くないとはいえ、無理して除去するべきか悩ましいケースも少なくありません。
例えば、 重複コードの前の部分で何か計算をおこなって、 その計算結果を重複コードの中で使っていると、その計算結果をくくりだした関数やメソッドに引数の形で渡さなければなりません。 重複コードの中で計算した結果をその後の部分で利用している場合は、 くくりだした関数やメソッドからそれを戻り値で返さなければなりません。 このようにやり取りする計算結果が増えてくると、 くくりだした関数やメソッドが段々読みにくくなってきます。
もっと悩ましいケースもあります。 図3はプログラムの2カ所に重複コードがある例ですが、図1と違い、 重複コードが完全に同じではなく、内部に一部異なる(オレンジの)部分があるケースです。 異なる部分があるので、単純に重複コードをくくりだして関数やメソッドにするわけにはいきません。

解決策として例えば図4のように、くくりだした関数やメソッドの中で、 異なる部分を if 文で切り替えて実行しなければなりません。 どちらを選んで実行するかは、関数やメソッドの引数で指定します。 もう少し気の利いた解決策は、重複コード内の異なる部分を関数オブジェクトにする方法です。 くくりだした関数やメソッドにこの関数オブジェクトを引数で渡せば、 完全に同一でない重複コードも関数やメソッドの形でくくりだすことができます。 オブジェクト指向言語なら、関数オブジェクトの代わりにメソッドの上書きを利用してもよいでしょう。

しかしこれらの解決策も完璧とは言えません。 重複コード内の異なる部分が何カ所にも増えてくると、どうやってもリファクタリング後のプログラムが読みやすいとは言い難くなってきます。
一般論を言えば重複コード(コードクローン)はない方がよいわけですが、 リファクタリングによって重複コードを取り除くべきか悩ましいことがあります。
大規模なプログラムになると、リファクタリングにはかなりの時間がかかります。 リファクタリング後のプログラムが正しく動くか、 リグレッションを起こしていないか、確認する作業も必要です。
上で述べたように、プログラミング言語の機能の限界により、 確かに重複コードは取り除けるが、 取り除いた後のプログラムが本当に読みやすくなっているか怪しいことがあります。
重複コードは似たような処理を書こうとして、 既存のコードをコピー&ペーストして(コピペして)少し修正することで生まれます。 プログラムの開発が進むにつれ、元は似たような処理であったはずのコード片が、 どんどん異なる処理をするように書き換えられていき、そのうち重複コードとは呼べなくなる、ということがよくあります。
ずっと重複コードのままなら、少々無理をしてでもリファクタリングして取り除いた方が良いでしょう。しかし、やがて重複コードでなくなるのなら、 そのままにして置いた方が良いでしょう。無理にリファクタリングすると、 かえって良くないかもしれません。 問題は、今、目の前にある重複コードが将来も残るのか、それとも自然に消えるのか、 ちょっと判断がつかないことです。
今すぐ取り除くべきか悩ましい重複コードは、ついついそのままにしてしまいがちです。 しかし、放置された重複コードはしばしば技術的負債として後から開発の重い足かせになります。 どこかで重い腰を上げて無理矢理でもリファクタリングするか、あきらめて技術的負債を払い続けるか、どちらかを選ばなければなりません。
ただ、どちらを選んでも不幸になる二択しか選べないのは、ソフトウェア開発の技術がまだ発展途上だから、と考えることもできます。 重複コードを取り除くためのリファクタリングは人力で作業するわけですが、 そういうリファクタリングは将来、自動化できて欲しいものです(コードのこの部分をメソッドにくくり出せ、のような指示さえ与えれば、今でも自動的にリファクタリングしてくれるのですから)。
もう一つ、重複コードを放置すると技術的負債となって跳ね返ってくるのですが、 それは重複コードの管理が人力だから、とも言えます。 重複コードが技術的負債となるのは、 互いに重複しているコード断片にバグがあったり、機能拡張が必要になって修正するとき、全てのコード断片をもれなく直すべきところ、人手による作業では一部の断片を直し忘れてしまうからです。
どれとどれが互いに重複コードとなっているか自動的に追跡して、修正のときに直し忘れを知らせてくれるツールがあれば、あまり負債を感じなくともすみそうです。 人手で一カ所を直せば他のカ所も同様に自動的に直してくれれば理想です。 今後そういうツールが発展して世に普及してくれば、 無理なリファクタリングならしなくてもよい、という主張もベストプラクティスの一つとして受け入れられるようになるやもしれません。 (株) Sider の CloneTracker もそういう世界を目指したツールの一つです。
重複コードの自動検出は簡単と思う人もいるかもしれませんが、完全に一致する重複コードを探すのならいざ知らず、 よく似ているが一致はしない重複コードを探すのは、さほど簡単ではありません。 またどのくらい似ていたら重複コードと考えるかは難しい問題で、重複コード検出には何十年もの研究の歴史があります。
コンピュータが生まれてから今日に至るまで、プログラミング言語はずっと進化してきました。 その進化の目的の一つは、ただでさえ行数が長くなりがちなプログラムをできるだけ短く簡潔に書けるようにして、プログラムのどこで何をしているかプログラマが(つまり人が)把握できるようにすることだったと思います。 そのために色々な構文や機能が発明されてきて、それらを駆使すればかなり複雑な処理をするプログラムであっても、 プログラマがきちんと把握できるようになってきました。 そういう構文や機能を使えば、例えば、 あまり似ていない重複コードであっても、一つの関数なりメソッドなりにまとめて重複をなくし、プログラムの見通しを良くできます。
しかしながらこれは「色々な構文や機能を駆使すれば」であって、人手を介在させて上手にリファクタリングすることが前提になっています。 プログラミング言語の色々な構文や機能は、あくまで人が使う道具であって、 使う人のスキルによって道具としての性能が左右されます。 だからこそ使いこなせるようにスキルを磨く楽しみが人間の方にはあるのですが、 生産技術としてみると手工業なので現代的とはあまり言えないかもしれません。
手工業の先には機械工業があるとすると、プログラミングにもそういう時代が来るかもしれません。 重複コードがあっても無理してリファクタリングせず、人手を介さずに自動的に重複コードの存在を追跡してすませる、というやり方を上に書きました。 もしかすると、こういうやり方の方が生産技術としては先進的なのかもしれません。 職人的なプログラマとしては少し受け入れがたい話ではありますが。
]]>pdf.js を使って pdf ファイルをブラウザに表示するプログラムを書いてみました。
Ruby on Rails 上で pdf を生成して出力する話はよくありますが、以下の話は Rails とは関係ありません。
ローカルで動かす Ruby プログラムから pdf ファイルを表示する、という話です。
適当な pdf ビューアを system メソッドで起動するのと違いはありませんが、あえて JavaScript のライブラリである pdf.js を使ってブラウザで pdf ファイルを表示しよう、という趣向です。
まず Jscall をインストールします。
gem install jscall
次のような URL の pdf ファイルを表示することにします。 なおブラウザで表示するので、この URL は任意のオリジンとの間でリソース共有 (CORS) を許可している必要があります。
https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf
Ruby のプログラムは次のようになります。
require 'jscall'
# 標準では node.js を使うのでブラウザを使うように指示
Jscall.config browser: true
# 最初は白紙の web page なので、以下の HTML コードをページに
# 書き込んで pdf の表示領域を用意
Jscall.dom.append_to_body(<<CODE)
<h1>PDF.js 'Hello, world!' example</h1>
<canvas id="the-canvas"></canvas>
CODE
# pdf.js をインポート
pdfjs = Jscall.dyn_import('https://mozilla.github.io/pdf.js/build/pdf.js')
# 表示する pdf ファイルの URL
url = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf"
pdf = Jscall.exec 'window["pdfjs-dist/build/pdf"]'
pdf.GlobalWorkerOptions.workerSrc = "https://mozilla.github.io/pdf.js/build/pdf.worker.js"
loadingTask = pdf.getDocument(url)
loadingTask.async.promise.then(-> (pdf) {
puts "PDF loaded"
# 最初のページを読み込む
pageNumber = 1;
pdf.async.getPage(pageNumber).then(-> (page) {
puts "Page loaded"
scale = 1.5;
viewport = page.getViewport({ scale: scale })
canvas = Jscall.document.getElementById("the-canvas")
context = canvas.getContext("2d")
canvas.height = viewport.height
canvas.width = viewport.width
# 読み込んだ pdf のページを表示
renderContext = {
canvasContext: context,
viewport: viewport,
}
renderTask = page.render(renderContext)
renderTask.async.promise.then(-> (r) {
# 表示が成功したらメッセージを出力
puts "Page rendered #{r}"
})
})
},
-> (reason) {
# エラーが発生した場合
puts reason
})
変数 url の値を例えば
url = "/doc/tiger.pdf"
のように変えると、ローカル・ファイルシステム上の ./doc/tiger.pdf が表示されます。
カレント・ディレクトリがルート / になります。
上のプログラムは pdf.js の例題の JavaScript プログラムをそのまま移植したものです。
var url = 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf';
var pdfjsLib = window['pdfjs-dist/build/pdf'];
pdfjsLib.GlobalWorkerOptions.workerSrc = '//mozilla.github.io/pdf.js/build/pdf.worker.js';
var loadingTask = pdfjsLib.getDocument(url);
loadingTask.promise.then(function(pdf) {
console.log('PDF loaded');
var pageNumber = 1;
pdf.getPage(pageNumber).then(function(page) {
console.log('Page loaded');
var scale = 1.5;
var viewport = page.getViewport({scale: scale});
var canvas = document.getElementById('the-canvas');
var context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
var renderContext = {
canvasContext: context,
viewport: viewport
};
var renderTask = page.render(renderContext);
renderTask.promise.then(function () {
console.log('Page rendered');
});
});
}, function (reason) {
console.error(reason);
});
二つのプログラムは Ruby と JavaScript の構文の違いをのぞけば、ほぼ同じです。
ただし Ruby のプログラムの場合、then の引数で渡されるハンドラ関数はあくまで Ruby の関数なので Ruby VM 上で実行されます。
puts の出力はブラウザのコンソールではなくて、Ruby を動かしているターミナル上に出力されます。
プログラム上、唯一の本質的な違いは JavaScript では
loadingTask.then
getPage(pageNumber).then
renderTask.promise
であるところが、Ruby では間に .async. が挿入されていることです。
つまり Ruby では、
loadingTask.async.then
getPage(pageNumber).async.then
renderTask.async.promise
のようになっています。
これは Jscall の仕様です。
Ruby はスレッド・モデルで動いていて基本的に同期的ですが、JavaScript は
promise を使っていて非同期的です。
この違いを自動的に吸収するように Jscall は作ってあるのですが、上の例では明示的に promise を返して Ruby 側も非同期的に動くようにしています。
その場合、promise を戻り値で返す JavaScript の関数を呼び出すときは、前に .async を付ける仕様になっているのです。
Jscall は非同期的な JavaScript ライブラリの振る舞いを、なるべく同期的に扱えるように努めるので、実は上に示したプログラムはもう少し簡単なプログラムに書き直すこともできます。
require 'jscall'
Jscall.config browser: true
Jscall.dom.append_to_body(<<CODE)
<h1>PDF.js 'Hello, world!' example</h1>
<canvas id="the-canvas"></canvas>
CODE
pdfjs = Jscall.dyn_import('https://mozilla.github.io/pdf.js/build/pdf.js')
url = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/examples/learning/helloworld.pdf"
pdf = Jscall.exec 'window["pdfjs-dist/build/pdf"]'
pdf.GlobalWorkerOptions.workerSrc = "https://mozilla.github.io/pdf.js/build/pdf.worker.js"
loadingTask = pdf.getDocument(url)
# ここまでは同じ
pdf = nil
pdf = loadingTask.promise
puts "PDF loaded"
pageNumber = 1;
page = pdf.getPage(pageNumber)
puts "Page loaded"
scale = 1.5;
viewport = page.getViewport({ scale: scale })
canvas = Jscall.document.getElementById("the-canvas")
context = canvas.getContext("2d")
canvas.height = viewport.height
canvas.width = viewport.width
renderContext = {
canvasContext: context,
viewport: viewport,
}
renderTask = page.render(renderContext)
r = renderTask.promise
puts "Page rendered #{r}"
何が違うかというとコールバック地獄に陥っていないということです。
このように書くと例えば pdf.getPage(pageNumber) は promise を返しません(間に .async. がないので)。
getPage は、ページの非同期的な読み込みが終了した後に戻り値を Ruby に返します。
同期的なのです。
戻り値は、元のプログラムでは then の引数に渡される関数が受け取るはずだった値です。
世の中には色々なライブラリがありますから、Ruby 以外の言語で書かれたライブラリを Ruby の中から使えれば色々と便利なこともあるでしょう。 Jscall は Ruby の中から pdf.js のような JavaScript のライブラリを使うためのライブラリです。 Ruby の中から Python ライブラリを使うための Pycall の親戚です。
]]>PyCall は Ruby の中から既存の Python ライブラリを使うにはとても便利なライブラリです。 とはいえ、これは Ruby から Python の関数を呼ぶためのライブラリなので、Python の関数を1回呼ぶ度に Ruby に制御が戻ってきます。 これが元でちょっと間違えやすいこともあるようです。 例えば下のような記事を見つけました。
Ruby×PyCallでTensorflowのMNISTチュートリアル「連想配列の違いで手間取った」
そこで Ruby の柔軟な構文を活かして、Python っぽいコードを Ruby プログラムの中に埋め込んでおくと、そこだけまとめて切り出して Python に送って実行する DSL を作ってみました。 Yadriggy を使って作っています。 この DSL の実装は内部で PyCall を使っていますから、要は PyCall のフロントエンドです。 なるべく Python で書かれたサンプルプログラムをそのまま Ruby プログラムの中にコピーして動くように DSL を設計してみました。
まず yadriggy をインストールしてください。バージョン 1.2.0 以上が必要です。
gem install yadriggy
当然、Python が必要です。Python 2 でも 3 でも動くはずです。
PyCall は標準では python コマンドで Python
インタプリタを起動するようですが、違うコマンドにする場合は環境変数をセットします。
例えば
export PYTHON=python3
のようにです。 また使いたい Python のライブラリもインストール済みでなければなりません。
この DSL を使ったプログラム例を示します。埋め込み DSL なので、以下は Ruby のプログラムです。
require 'yadriggy/py'
# draw_pie() は Python の関数
def draw_pie(labels, sizes, explode)
fig1, ax1 = plt.subplots()
ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)
ax1.axis('equal')
plt.show()
end
def run()
# 以下 3 行は Ruby のコード
Yadriggy::Py::Import::import('matplotlib.pyplot').as(:plt)
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'
sizes = [15, 30, 45, 10]
Yadriggy::Py::run do
# このブロックの内部は Python のコード
ex = tuple(0, 0.1, 0, 0) # tuple が必要
draw_pie(labels, sizes, ex)
end
end
run
(この例題は Matplotlib のチュートリアルから引用しました。)
上のプログラムの中で DSL で書かれたコードなのは、Yadriggy::Py::run
の引数ブロックの中の
# このブロックの内部は Python のコード
ex = tuple(0, 0.1, 0, 0) # tuple が必要
draw_pie(labels, sizes, ex)
この部分と、この中で呼ばれている draw_pie メソッドの定義
# draw_pie() は Python の関数
def draw_pie(labels, sizes, explode)
fig1, ax1 = plt.subplots()
ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)
ax1.axis('equal')
plt.show()
end
です。
プログラムが実行されて Yadriggy::Py::run メソッドが呼ばれると、引数の
DSL コードは Python 側へ送られて実行されます。
draw_pie メソッドは Yadriggy::Py::run の引数のブロックの中のから呼ばれているので、DSL
コードの一部と見なされて一緒に Python 側へ送られます。
これらに加えて、変数 labels, sizes, ex の値もコピーされて Python 側へ送られます。
同様に Yadriggy::Py::run の引数のブロックの中で参照されているからです。
draw_pie メソッドの定義は Ruby の構文を使って書かれていますが、あくまで DSL
のコードですので、Python のコードとして解釈されます。
例えば ax1.pie() の引数の explode=explode は Python のキーワード引数として解釈されます。
上記のプログラムが matplotlib.rb とすると、
ruby matplotlib.rb
のように実行すれば円グラフが表示されます。

ちょっと便利… でしょうか?
対応する元の Python プログラムは
import matplotlib.pyplot as plt
def draw_pie(labels, sizes, explode):
fig1, ax1 = plt.subplots()
ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)
ax1.axis('equal')
plt.show()
def run():
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'
sizes = [15, 30, 45, 10]
ex = (0, 0.1, 0, 0) # tuple は不要
draw_pie(labels, sizes, ex)
run()
です。
DSL コードと比較すると、違いは変数 ex の初期値の tuple を作るとき、DSL コードでは
tuple(...) と書かなければならないことと、import 文がメソッド呼び出しの鎖になっていることぐらいです。
Import 文は
Yadriggy::Py::Import::import('matplotlib.pyplot').as(:plt)
となっています。
それからメソッド(関数)定義が def … end の組になっているところも Python とは異なります。
結構似るように DSL を作ったつもりですが、まったく同じ、というわけではありません。
このように、いかに Ruby の構文が柔軟とはいっても、Python コードそのままでは構文エラーになってしまう部分が少なからずあります。 このため、任意の Python コードをそのままコピーして Ruby の中に埋め込めんでも動く、というわけにはいきません。 詳しくは Wiki を見てください。
PyCall には PyCall::exec や PyCall::eval といったメソッドがあり、文字列で渡した
Python コードを実行してくれます。
実は本 DSL も、最終的にはこれらのメソッドを使って Python のコードを実行しています。
なんだ PyCall::exec 等があれば十分だったではないか、と思うかも知れませんが、本 DSL
を使うとコード中で参照されている変数の値も一緒に Python へコピーされます。
例えば
def run()
Yadriggy::Py::Import::import('matplotlib.pyplot').as(:plt)
labels = 'Frogs', 'Hogs', 'Dogs', 'Logs'
sizes = [15, 30, 45, 10]
PyCall::exec <<CODE # コードを文字列として渡すので string embedding
ex = (0, 0.1, 0, 0)
draw_pie(labels, sizes, ex)
CODE
end
一見よさそうに見えますが、このようにすると、変数 labels, sizes, ex の値は Python
側へコピーされません。
おそらくそれらの変数が Python では未定義であるというエラーになるでしょう。
本 DSL ではそれらの変数の値もコピーされますので、エラーにはなりません。
この DSL の作っているとき、周囲に話をすると、そんな DSL 何の役に立つのか、とよく聞かれました。 実用性という観点からは、PyCall があれば十分かもしれません。 が、Ruby プログラムの中に Python っぽいプログラムが同居していて、目的に合わせて適した方を使う、というのも悪くないと思いませんか? … 思わないか。
]]>Yadriggy を使うと power assert を簡単に作ることができます。 なので作ってみました。
Power assert は assertion が失敗したとき、assertion の各部分式の 値を表示してくれる関数かと思います。 Groovy や .NET、JavaScript など様々なプログラミング言語で使え、もちろん Ruby でもすでに使えます。 既存の Ruby 実装はけっこう凝った実装ですが、Yadriggy を使えばわりに素直に実装できます。
Yadriggy 版の power assert も使い方は簡単です。
require 'yadriggy/assert'
arr = [2, 4, 6]
Yadriggy::Assert::assert { arr[1] % 2 != 0 }
Assertion は assert メソッドにブロックの形で渡します。
渡された assertion が失敗すると、assert メソッドは assertion 中の各部分式の実行結果を表示します。
--- Yadriggy::Assert ---
arr[1] % 2 != 0
| | | | |
| | | | 0
| | | false
| | 2
| 0
4
------------------------
自分流の power assert メソッドを定義すると便利なことがあります。
Yadriggy 版の assert メソッドの実装 は下のようになります。
def self.assert(&block)
reason = Reason.new
begin
res = assertion(reason, block)
puts_reason(reason) unless res
return res
rescue AssertFailure => evar
puts_reason(evar.reason, evar)
raise evar.cause
end
end
このメソッドの主要部は assertion メソッドの呼び出しです。
res = assertion(reason, block)
assertion は assertion を実行して結果を返します。
最初の引数 reason は assertion の実行結果が記録されるオブジェクトです。
2番目の引数 block は assertion を含むブロックです。
これを使えば次のようなメソッドを自分で定義できます。
def my_assert(&block)
reason = Yadriggy::Assert::Reason.new
unless Yadriggy::Assert::assertion(reason, block)
puts(reason.show) # 実行結果を表示
binding.pry # pry で reason をさらに調査する
end
end
reason.show は String の配列を返します。
これは assertion の実行結果を表します。
reason.ast は assertion の抽象構文木です。
reason.results は hash 表で、部分式からそのソースコードおよび値を得るのに使います。
例えば reason.results[reason.ast] は、最初の要素が ast のソースコード、
2番目の要素が ast の実行結果である配列を返します。
同様に下のように各部分式の値を調べることができます。
部分式の値が大きなオブジェクトで、reason.show では大量のテキストが表示されてしまう場合に便利でしょう。
ast = reason.ast
results = reason.results
results[ast][1] # assertion の値
# もし ast が + や < などの2項演算子式の場合
results[ast.left][1] # 左辺の値
results[ast.right][1] # 右辺の値
# もし ast が ! などの単項演算子式の場合
results[ast.operand][1] # オペランドの値
# もし ast がメソッド呼び出しの場合
results[ast.receiver][1] # レシーバの値
results[ast.args[0]][1] # 第1引数の値
# もし ast が括弧式 (...) の場合
results[ast.expression][1] # 式の値
Yadriggy を使った実装は単純です。 ソースコードのプリプロセシングや専用の仮想機械は不要です。
まず assertion を含むブロックの抽象構文木を取り出します。
ast = Yadriggy::reify(block)
次にその抽象構文木をたどって、変数や直接解釈実行できない複雑な式のノードにあたったら、そのノードをソースコードに変換します。
src = Yadriggy::PrettyPrinter.ast_to_s(ast)
ここで ast は木のノード(葉あるいは中間ノード)です。
ast_to_s は抽象構文木に対応するソースコードを String オブジェクトの形で返します。
その後、eval でソースコードを実行します。
file_name, lineno = ast.source_location
eval(src, block.binding, file_name, lineno)
ast.source_location は ast のソースコードのファイル名と行番号を返します。
このような power assert の実装もドメイン専用言語 (DSL) と見なすことはできると思います。
Ruby のセマンティクスを少し拡張したセマンティクスの Ruby 風言語、というわけです。
セマンティクスはほぼ同じですが、部分式を実行する度に結果が Reason オブジェクトの中のログに記録される点が異なります。
こういうある種のログ記録システムは昔はアスペクト指向と呼ばれることもありましたが、DSL と考えてもよいのではないでしょうか。
Ruby は埋め込み DSL (embedded DSL, eDSL) のホスト言語としてよく使われます。 今回試作した JIT コンパイラは、Ruby 向けに作った C 言語風の eDSL 用の JIT コンパイラです。 そういう DSL は、構文だけホスト言語である Ruby の構文を借りて、実行は自前の JIT コンパイラを使うので、我々はそのような DSL を 半パラサイト DSL (hemi-parasitic DSL) と呼んでいます。
この eDSL は、Ruby プログラムの中で、一部の計算処理を高速実行したいときに使う eDSL です。 C 言語で関数を書き、それを ruby-ffi で呼び出してもよいのですが、そのあたりの作業を簡単にすることを目的としています。
もちろん Ruby VM の JIT コンパイラを開発して高速化することも大切なのですが、Ruby のセマンティクスを厳密に守りつつ高速実行するのは大変なはずで、ちょっと JIT コンパイラを書けばよいというものではありません。ですから、Ruby のセマンティクスはあまり守らずに、都合の悪いところは無視して高速なコードを生成する、というのは実用上は意味のあることかと思います。 なお今回の eDSL の場合、見た目は Ruby ですがセマンティクスはかなり異なります。 Ruby のセマンティックスはあまり守らず、というようより、全く異なる言語と思ってください。
簡単な例として Fibonacci 数を計算するプログラムを示します。
require 'yadriggy/c'
include Yadriggy::C::CType
def fib(n) ! Integer
typedecl n: Integer
if n > 1
return fib(n - 1) + fib(n - 2)
else
return 1
end
end
puts Yadriggy::C.run { return fib(32) }
fib メソッドが eDSL で書かれた部分です。
C 言語風なので静的に型付けされます(Ruby でないので!)。
def の行の ! のオペランドが戻り値の型で、typedecl で引数 n の型を宣言しています。
! ではなく - にした方が格好がよかったかもしれませんが、Ruby のメソッドとしても実行できるようにと思って ! を選びました。
上の例ではでてきませんが、局所変数がでてくる場合は、これも typedecl で型を宣言します(現仕様では、局所変数がある場合は引数の型宣言のための typedecl とは別の行に typedecl を書きます)。
ただし簡単な型推論はしますから、局所変数の型はしばしば省略可能です。
最後の行の run メソッドでブロック
{ return fib(32) }
を渡しています。本 eDSL は Yadriggy (宿り木、です)というライブラリを使って実装しているのですが、このライブラリは、ブロックのソースコードを見つけてきて読み込み、抽象構文木を作ります。
さらにこのブロックの中から呼ばれている fib も eDSL の一部と認識して、その抽象構文木も作ります。
eDSL の JIT コンパイラは、この抽象構文木から C 言語のソースを生成し、コンパイルして、得られたバイナリを ruby-ffi を使ってリンク、呼び出します。
fib(32) の実行結果は return によって Ruby 側に戻され、表示されます。
この eDSL は C 言語風なので、全ての戻り値は return 文で戻さなければならないことに注意してください。
この他の例は Wiki をごらんください。 配列を使った例や OpenCL を用いた例についての説明もあります。
最後に、なんで Ruby を選んだの? と聞かれることがありますが、それは Ruby の構文は(変態的^H^H^H非常に)柔軟で、かなりの範囲の構文を表現できるからです。
Yadriggy を作る前に Java で同様のものを作ったのですが、Java 言語は構文の自由度が小さく、また静的に型付けされるので、あまり Java とは異なる DSL を埋め込んでいる、という風になりませんでした。
]]>