Let's start Scheme

2014-12-01

R7RSポータブルライブラリを書く際の落とし穴

この記事はLisp Advent Calendar 2014の2日目として投稿されました。

R7RSでは処理系毎の差異を吸収可能な構文cond-expandが採用されポータブルなコードが書きやすくなった。では実際のところどれくらい書きやすくなったかという話は寡聞にして聞かないので、ある程度実用的なライブラリを書いて検証してみた。結論から先に書くと、R6RS時代とそこまで変わっていないか、多少不利というのが僕個人の感想である。その理由を一つずつ見ていくことにしよう。

【ライブラリ拡張子】
可搬性の高いライブラリを書くのであれば、外せないポイントの一つである。R7RSでは既定されていない部分であるため、最大公約数を選ぶしかない。知る限りではSagittariusとGaucheは拡張可能である。参照実装であるChibi Schemeが.sldを採用したのでそれに追従する処理系が多い(例:Foment)。再度R7RSでは既定されていないので、処理系依存になる。例えばpicrinは.sldをサポートしていないし、しばらくサポートされなさそうである。これも踏まえてポータブルに書くとなると、実装とライブラリ定義は完全に切り離す必要がある(Chibiは常にこの方式を採用している)。しかしなが、それでは実装者の負担が大きくなるので線引きをする必要はある。現状であれば.sldを採用するのが妥当なところであろう。

余談ではあるのだが、Gaucheでライブラリ拡張子を追加するのは-eオプションを使う必要がある。具体的には-e '(set! *load-suffixes* (cons ".sld" *load-suffixes*))'のようなものが必要になる。append!でもいいのだが、そうすると.scmの方が優先順位が高くなるので嵌ることがある。(嵌った)

【サポートされてるSRFI】
R7RSの範囲だけでもある程度のことはできるのだが、SRFIくらいは許容範囲に入れないとある程度の範囲が狭い。例えばマルチスレッドやソケット等はSRFIを使わないと完全に処理系依存になる。
しかし、これがポータブルなライブラリを書く際の落とし穴になる。 例えば僕が書いたライブラリは以下のSRFIを要求する:
  • SRFI-33(withdrawn)/SRFI-60もしくは(rnrs)
  • SRFI-106
  • SRFI-19(サポートされていれば)
この中に文字列を扱うSRFI-13がないのには理由がある。Chibiがサポートしていないからである。(ちなみにFomentもサポートしていない。) 参照実装から持ってくるという方法もあるといえばあるのだが、ロードパスの問題も出てくる。例えばFomentはSRFI-1もサポートしていないがChibiはしているので、サポートしていないSRFIだけを入れるというのは困難である。特に組み込みでサポートしている場合は参照実装に置き換えると性能が落ちる可能性が出てくる。

ここでは問題としてChibiを指しているが、SagittariusにもあってSRFI-60をサポートしていないのである。理由は面倒だからの1点なのだが、流石にちょっと無視できなくなってきたかもしれない。自分で自分の足を打ち抜いてる感がすごいので*1・・・

余談だが、ChibiはSRFI-106をサポートしていない。なので処理系依存のコードが貼り付けてある。

【R6RSにあってR7RSにない手続き】
R7RSはR6RSで定義された手続きの大部分を提供しない。 特にバイトベクタ周りの手続きがごっそり抜けている。例えばbytevector-u32-refとかである。単に互換手続きをScheme側で実装してやればいいだけなのでそこまで問題にならないが、これがbytevector-ieee-double-refとかだと骨が折れること間違いなしだろう。(今回は16ビット整数と32ビット整数だけだったのでそこまででもなかったが。)

R6RSに限った話ではないが、処理系によってはライブラリもしくは組み込みで提供されている手続き等があり、それらは性能を出す上で重要になってくるかもしれない。例えば上記のバイトベクタ周りの手続きはSchemeで実装した場合複数回のメモリ割付が必要とされる可能性があるが(bytevector-u64-ref等)、処理系が組み込みでサポートしていた場合にはメモリ割付は1回に抑えられるかもしれない。タイトなループで呼ばれる際には無視できない要素になる可能性がある。

【ポータブルなライブラリを書くにあたって】
ではどうするか?というのはライブラリを書く上で重要になるだろう。残念ながら、これといった指標のようなものは今のところ僕個人の中にはない。そして、残念ながらR7RSポータブルなライブラリの絶対数も少ないこと等、既存のものから学ぶという方法もとりづらい。今後の参考になるよう今回書いたライブラリから学んだ点を列挙する。
  1. R7RS外の機能の分離
  2. サポートする処理系の具体的イメージ
  3. 習熟度の低い処理系の性能はあきらめる
#1はサポートされていないSRFIの救済を含む。ライブラリの要件に必要なSRFIを列挙すればいいのだが、多くの処理系で使えるようにするにはそれすらも最小限に抑えた方がよい。例えば今回の例ではSRFI-13があるが、必要だった部分は非常に小さかったのでライブラリ側で実装し依存度を減らした。

#2は対象とする処理系をある程度列挙することである。今回のライブラリではスタートポイントとしてSagittarius、GaucheそしてChibiを選択した。それぞれの処理系に癖があり、細かい点で嵌ったものもある。例えばGaucheのソケットポートはバッファを確保するのでコマンドを任意のタイミングでサーバに送る際にはバッファをフラッシュする必要があった。(他にもwith-exception-handlerがSEGVるとかあるが、それはバグを踏み抜いただけということで。) 複数の処理系で走らせることができれば処理系が許容する未定義動作をある程度意識して回避することが可能である。R7RSでは未定義動作の範囲が広く、また処理系の拡張をかなり許しているため、ポータブルなコードを書く際にはこれを踏まないようにするのが鉄則になるだろう。(今回の動作はSRFI-106なので、自分で足を打ち抜いたのではあるが・・・)

#3は性能を出すポイントというのは処理系ごとに違うので習熟度が低い処理系であれば、ポータブルに書くことを優先すべきである。今回は重たい処理(MD5ハッシュやバイナリ変換)に関してSagittariusでのみ処理系が用意している(バイナリ変換はR6RS由来だが)手続きを用いた。GaucheにもMD5ハッシュはあるが、R7RS+SRFIの範囲で書かれているものを流用することにした*2。当然だが、サポートする全ての処理系に依存するコードで書いたとしても可能な限りポータブルなコードは残さなければならない。仮にR7RSやSRFIで既定されていないものであっても、既定の動作としてエラーを投げるものを組み込んでおくだけでライブラリの一部が使えたり、後のサポートを容易にすることが可能である。

上記のポイントに加えて、ある機能を処理系依存で切り分ける際に必ず一つはR7RS+SRFIの範囲に動くようにした。それぞれの処理系の癖やバグを回避する際に必ず一つはポータブルなパスを通るようにすることで、可能な限りポータブルに出来たと思う。(スペックだけを見るのであれば、Fomentでも何の変更もなく動くはずである。 )

【結論】
R7RSに限ったことではないが、ポータブルなライブラリを書くことは非常に大変である。以前にR6RSポータブルなライブラリを書いた際にも同様な感想を持ったが、R7RSは決定からの時間が浅いからか、カバーする範囲が狭いからか、処理系ごとの癖が強いからなのか分からないがR6RSのときよりもライブラリ本体ではない部分のコードを書く量が多かった気がする。R6RSポータブルなライブラリはJSONを扱うものなのでその差かもしれないが(サポートした処理系全てがかなりの数のSRFIをサポートしていたというのも大きな要因かもしれない)。しかしながら、ある程度の制限はあるもののポータブルなライブラリを書く手段が言語レベルで既定されているというのはやはり大きな利点であると思われる。

長々と書いたが一言でまとめれば、もっとR7RSなSchemeを使おうということになる。もちろんR6RSでも構わない。

*1: 入れた。次のリリースからは使えるようになる。
*2: 正直なところ、GaucheやChibiをタイトに使うことはほとんどないので、Sagittariusで性能が出ればいいというのもある。他の処理系で性能がいる場合はPull Reqを送っていただけるとありがたい。

No comments:

Post a Comment