pureなpackage
時代は純粋な関数だよ!
状態なんて持ってちゃ駄目だね。
いきなりハイテンションですね。
そんな風に冷めた態度で、カッコいいとでも思ってるのかい。
人間ピュアな心を無くしたら終わりだよ、やっぱり純粋なのが一番だよ、うんうん。
……突っ込んでも長引くだけだなあ……もういいや。
で、一体何をどうしたんですか。
リファレンスを見ていたらpragma Pure;というのを発見したんだ。
これを使えば状態のないコードしか通らなくなるんだ、すごいだろう。
Adaは、というよりもgccは、末尾呼び出しの最適化も保証されてませんし、Dのようにコンパイル時の展開を強制できるわけでもないですから、本当に状態のないコードしか通らなくなるだけですけどね……。
素直に関数型言語を使えばいいじゃないですか。
あれ、そうなんだ?
でもAdaの成立年代を考えたら先進的じゃないか。
状態を無くして参照透明なわかりやすいコードを、だよ。
別に使うのを止めてるわけでもありませんよ。
なんだい、張り合いがないなあ。
これを使ってがんがん書いちゃうもんね、純粋なコードを。
どうぞどうぞ。
unsafePerformIOが欲しい!
いつまで持つかな……。
あ、戻ってきましたね。
unsafePerformIOはないのかい!?
unsafe...なんですって?
君も物を知らないなあ。
純粋なコードから副作用のあるコードを呼び出すための仕組みだよ。
無いとデバッグに不便じゃないか。
デバッガをどうぞ。
デバッグ用に書き換えたコードで正常動作してるように見えても、デバッグできたとは言えません。
試したよ。ちょっとトレースしたらgdbごと落ちるんだよ。
流石、安心のクォリティ。
勿論そんなことだろうと思ってました。言ってみただけですよ。
わかってるなら言わないでくれ……。
そういうわけで、printfデバッグしか道は残されてないんだけど、もちろんpureなpackageからはText_IOは使えないし。
pragma Pure;を使っても使わなくても機能的に変わらないということは、副作用のあるコードを混ぜても実際には不都合ないってことだよね。当然あるんだろう、抜け道が。無いと不便だもんね。
pragma Pure;をコメントアウトすればいいじゃないですか。一番手っ取り早い。
簡単に言わないでくれよ。
僕が書いているのは20以上のファイルからなる壮大なステートレスライブラリだぞ。
pureなpackageはpureなpackageしか参照できないんだから、ひとつ外すと泥縄式に全部外して回らないといけないじゃないか。
デバッグのたびにいちいちそんなことできないよ。
なんでAdaなんかを使っているのでしょう、この男は……。
君にそんなことを言われるとは思ってなかったよ。自分のことじゃないかい。
pragma Pure;を付けたままでできる手っ取り早い出力方法を教えてくれよ、なあ。
PreelaborateならまだGNAT.IOも使えたんですけどねえ……とりあえずAssert?
そんなの失敗した場所しかわからないじゃないか。
引数の値を表示したいんだよ。
まさか何も用意されてないってことは無いんだろう?
まあ待ってくださいよ、grepかけてますから……adaincludeの下でPureとPutを同時に含むファイルは……と。
うん、無いです。
なんだってー!
そんなのってないよ。どうすればいいんだ……。
最後の抜け道
ちょっと意地悪が過ぎたでしょうか。
お、やっぱりあるんだ?
そうだよなあ、デバッグ出力は必須だよ、うんうん。
デバッグ出力用の機能ではありませんけどね。
勿体付けないでくれよ。
で、なんて名前のpragmaなんだい。ライブラリでは無いんだよね。それとも特殊な属性?
printfという名前の関数です。
Adaのライブラリではありませんが、libcに収録されているため何も考えなくてもリンクされ、どこからでも使える便利な関数です。
……おい。
それはもしかして……。
もしかしなくてもstdio.hにあるおなじみのprintfですよ。
書式化文字列のインタープリタが組み込まれていて、数値のフォーマットや、アドレスの16進表示までしてくれる大変優れものです。
そんなの説明してくれなくても分かりきってるよ。
なんでText_IOが駄目なのにprintfなら呼べるんだい。
ははは。
pragma Import (C, printf);って書けば、pureなpackageからでもどこからでも、C言語の関数でもシステムコールでもなんでも呼べます。
Windows APIならCの代わりにStdcallです。
要するに自分で外部呼び出しを書けってことか……。
HaskellでもFFIで呼ぶ関数の返値にIOを付けるかどうかは好きにできたもんなあ……。
printfのシグネチャその1
ええと……まず第一引数がchar const *で……返値がintで……。
function printf (fmt : Interfaces.C.chars_ptr; ...) return Interfaces.C.int;
pragma Import (C, printf);
可変長引数ってどう書けばいいの?
C言語の可変長引数と呼び出し規約レベルで互換性のある可変長引数はありません。
ですので、必要なだけオーバーロードすることになります。
必要なパターンを全部書けってことね。
欲しいのは整数と、実数と、文字列と、あと引数のアドレスも一応表示させたいな。
可変長引数では実数は全部doubleだから……。結構記述量がいるなあ。
これでどうだ!
function printf (fmt : Interfaces.C.Strings.chars_ptr; a : Interfaces.C.int) return Interfaces.C.int;
pragma Import (C, printf);
function printf (fmt : Interfaces.C.Strings.chars_ptr; a : Interfaces.C.double) return Interfaces.C.int;
pragma Import (C, printf);
function printf (fmt : Interfaces.C.Strings.chars_ptr; a : Interfaces.C.Strings.chars_ptr) return Interfaces.C.int;
pragma Import (C, printf);
type int_ptr is access Integer;
pragma Convention (C, int_ptr);
function printf (fmt : Interfaces.C.Strings.chars_ptr; a : int_ptr) return Interfaces.C.int;
pragma Import (C, printf);
あれ、エラーだ。
p.ads:1:06: cannot depend on "Strings" (wrong categorization)
p.ads:1:06: pure unit cannot depend on non-pure unit
p.ads:8:04: entity "printf" was previously imported
p.ads:8:04: (pragma "import" applies to all previous entities)
p.ads:8:04: import not allowed for "printf" declared at line 5
p.ads:10:04: entity "printf" was previously imported
p.ads:10:04: (pragma "import" applies to all previous entities)
p.ads:10:04: import not allowed for "printf" declared at line 7
p.ads:11:09: named access type not allowed in pure unit
p.ads:11:09: would be legal if Storage_Size of 0 given
p.ads:14:04: entity "printf" was previously imported
p.ads:14:04: (pragma "import" applies to all previous entities)
p.ads:14:04: import not allowed for "printf" declared at line 9
ひとつずつ解決していきましょう。
まずpragma Importはひとつでいいです。
そうなんだ。
今回みたいな用途を想定してるのかな。
sinfやsinlみたいな関数をAda側で全部sinにしたいときはどうするの?
別々の関数名でインポートして、後で同じ名前にrenamesですね。
よし、だいぶ減ったぞ。
p.ads:1:06: cannot depend on "Strings" (wrong categorization)
p.ads:1:06: pure unit cannot depend on non-pure unit
p.ads:8:09: named access type not allowed in pure unit
p.ads:8:09: would be legal if Storage_Size of 0 given
あれ、pureなpackageの中ではアクセス型は使えないのか……。
Interfaces.C.Stringsもpureじゃない……ということは文字列が渡せない、ということはprintfなんて呼べないじゃないか、嘘つき!
だから日頃からInterfaces.Cは使えないと言ってるじゃないですか。
なんでInterfaces.C.Stringsはpureではないんだろう。
pureでいいように思うんだけど。
メモリを割り当てたり開放したり、ヒープ自体グローバル変数のようなものですからね。
関数型言語では純粋ってことにされてますけど。
アクセス型も同じですよ。
このint_ptrはnewを使うつもりはないんだけどなあ。
既にある変数の'Accessを渡したいだけで。
エラーメッセージにあるように、for int_ptr'Storage_Size use 0;って書けばnewしないことになってpureなユニットでも使えます。
でも、もっと短くて済む方法がありますよ。
どうするんだい?
匿名のアクセス型にしてしまえばOKです。chars_ptrも同じですよ。
function printf (fmt : access constant Interfaces.C.char; a : Interfaces.C.int)
return Interfaces.C.int;
function printf (fmt : access constant Interfaces.C.char; a : Interfaces.C.double)
return Interfaces.C.int;
function printf (fmt : access constant Interfaces.C.char; a : access constant Interfaces.C.char)
return Interfaces.C.int;
function printf (fmt : access constant Interfaces.C.char; a : access constant Integer)
return Interfaces.C.int;
pragma Import (C, printf);
おお、コンパイルが通ったぞ。
呼び出してみる
早速呼び出そう。
ええと……フォーマット文字列を用意して、先頭アドレスを渡すから……。
declare
fmt : Interfaces.C.char_array := "debug: %d\n";
dummy : Interfaces.C.int;
begin
dummy := printf (fmt (0)'Access, Interfaces.C.int (Argument));
end;
お、何か出た。
わわわ、出力が止まらない……止まった。
Adaの文字列は特に\0で終わったりしませんからね。
D言語なんかでも事情は同じです。
あと\nも意味ないですって。
面倒だなあ……。
declare
fmt : Interfaces.C.char_array := "debug: %d" &
Interfaces.C.char'Val (10) & Interfaces.C.NUL;
dummy : Interfaces.C.int;
begin
dummy := printf (fmt (0)'Access, Interfaces.C.int (Int_Argument));
end;
よし、ちゃんと出たぞ。この値は正しそうだ。
こっちの文字列も出力してみるか。
型が合わないからこのTo_Cってやつで……ああもう、関数の返値に直接'Accessって書けないのか……まあそりゃそうか……。
declare
fmt : Interfaces.C.char_array := "debug: %s" &
Interfaces.C.char'Val (10) & Interfaces.C.NUL;
dummy : Interfaces.C.int;
a : Interfaces.C.char_array := Interfaces.C.To_C (Str_Argument);
begin
dummy := printf (fmt (0)'Access, a (0)'Access);
end;
次はこっちの引数を……ああもう、面倒くさい。
バグが取れるといいですね……それじゃあ。
printfのシグネチャその2
待った、さっきからニヤニヤして人の苦労を楽しんでるぞ。
絶対まだなにかあるだろう。
ちっ……。
舌打ちするなー!
ははは、あまりにも君が真っ正直なコードを書くもので。
そうだった、こいつは正直者を笑うような奴だった、鬼……。
いえ、正直なのは美徳ですよ……本気でそう思ってますって……。
でも今回はデバッグ用なんですから、移植性なんてどうでもいいですよね。
コンパイラをgccに限れば、もっとずっと楽になります。
よし、そいつを教えてもらおうじゃないか。
人の事を笑ったんだからな、きっちりしっかり説明させてやる。キリキリ吐け。
はいはい……。
はいは一回。
はい……。
で、どこを省略できるんだい。
まず、Interfaces.Cを使うのをやめましょう。何度も使えないって言ってますのに。
withを追加するのも手間でしょう。
やめちゃうと、引数に使う型のpragma Convention (C, ...);バージョンを自分で書かないといけないじゃないか。
全部再宣言してライブラリとして持っておけばいいのかもしれないけど、デバッグ用にちょっと書き足すだけなんだから、かえって手間なように思うけど。
gccの場合、ほとんどの型ではpragma Convention (C, ...);では何も変わりません。Adaの関数もCの関数も同じ呼び出し規約です。
AdaのStandardの型とCの基本型も対応が取れてますので、そのまま使えます。
……なんとなくそんな気はしてたよ……じゃあこれでいいのかい。
function printf (fmt : access constant Character; a : Integer) return Integer;
function printf (fmt : access constant Character; a : Long_Float) return Integer;
function printf (fmt : access constant Character; a : access constant Character) return Integer;
function printf (fmt : access constant Character; a : access constant Integer) return Integer;
pragma Import (C, printf);
declare
fmt : String := "debug: %s" & ASCII.LF & ASCII.NUL;
dummy : Integer;
a : String := Str_Argument & ASCII.NUL;
begin
dummy := printf (fmt (0)'Access, a (0)'Access);
end;
p.adb:10:27: prefix of "access" attribute must be aliased
p.adb:10:43: prefix of "access" attribute must be aliased
あれ、Stringはaliasedが付いてないぞ。
拡張で'Unrestricted_Accessというのもあります。
ですが、pragma Import (C, ...)の関数に配列を渡すときは先頭アドレスが渡ることになっていますので、こうしちゃいましょう。
function printf (fmt : String; a : Integer) return Integer;
function printf (fmt : String; a : Long_Float) return Integer;
function printf (fmt : String; a : String) return Integer;
function printf (fmt : String; a : access constant Integer) return Integer;
pragma Import (C, printf);
declare
dummy : Integer;
begin
dummy := printf (
"debug: %s" & ASCII.LF & ASCII.NUL,
Str_Argument & ASCII.NUL);
end;
……お、驚かないぞ。驚かないぞ。
でもAdaの配列は範囲付きで渡されるんじゃなかったのかい。
ほとんど、の例外ですね。
C関数ではこの方が便利だからこうなっているのでしょう。
後は、返値がレジスタに入る型なら、無視してもOKですね。返値が構造体の場合は隠しパラメータになったりしてるかもしれませんけれど。
procedure printf (fmt : String; a : Integer);
procedure printf (fmt : String; a : Long_Float);
procedure printf (fmt : String; a : String);
procedure printf (fmt : String; a : access constant Integer);
pragma Import (C, printf);
printf (
"debug: %s" & ASCII.LF & ASCII.NUL,
Str_Argument & ASCII.NUL);
すごく短くなってしまった……。
あくまでgccのみですよ。
C_Pass_By_Copy
他にほとんど、の例外はあるの?
こんな感じでしょうか。
規約 | 基本型 | ポインタ | 配列 | 構造体 |
C | 値渡し | 値渡し | 先頭アドレス | 値渡し |
Ada(C) | 値渡し | 値渡し | 先頭アドレス | 先頭アドレス |
Ada(C_Pass_By_Copy) | 値渡し | 値渡し | 値渡し | 値渡し |
Ada(Ada) | 値渡し | 値渡し | 先頭アドレスと範囲 | 不定 |
えー、pragma Convention (C, ...);では構造体は参照渡しされるんだ?
C言語では値渡しなのに……。
だからC_Pass_By_Copyを使いわけないといけなくて手間なんですよね。全く何を考えているのだか、完全に合わせてくれたらいいのに……。
まあ、C関数に渡すrecordにはpragma Convention (C_Pass_By_Copy, ...);を付ける、それ以外はpragma Convention (C, ...);か、或いは何も付けないか、と、それだけの使い分けですが……。
Interfaces.C
でもgccに限定しないことを考えたら、やっぱりInterfaces.Cみたいなものは必要だと思うけどなあ……。
そういったものが必要なのは否定しません。
Interfaces.Cが使えないんですよ……。
なんで?
理由は色々ありますが……。
例えば、Strings.chars_ptrとPointers.Pointerが何の関係もない型として宣言されているから、chars_ptrに対してポインタ演算できないとか、そのPointersにはDefault_Terminatorを渡さないといけないから、文字列以外のポインタ演算をしようとしたときに必要のないダミー値を作らないといけないとか、結果Interfaces.CとStringsとPointersで文字列操作用の関数がそれぞれ微妙に違う仕様で被ってるとか……。そんな風に色々用意していながら、string.hの関数をインポートして使うほうが遥かに使い勝手が良いところなんか終わってますね……。
あとchar16/32_tにCの規格が決まるよりも先にフライング対応しておきながら、_Boolが無いとかlong longが無いとか複素数が無いとか。boolとlong longはgcc(GNAT)はInterfaces.C.Extensionsである程度補ってくれてますが。
わかった、わかったよ。
でもどれも些細なことのようにも思えるけど。
一番使えないのは、GNATのwchar_tの定義です。
規格ではなく実装の問題ですが。
規格では「type wchar_t is <implementation-defined character type>;」と書かれてるね。
実装ではこうなってます。
gccはマルチプラットフォームで、GNATは更に他のバックエンドにも手を伸ばしてますが、この宣言はすべてのターゲットで共通です。
type wchar_t is new Wide_Character;
for wchar_t'Size use Standard'Wchar_T_Size;
問題ないように思えるけど。
Standard'Wchar_T_Sizeは、その環境のwchar_tのサイズになるんだよね。
問題ありまくりですって。
Cのwchar_tは環境依存ですが、AdaのWide_Characterは16ビット固定なんですよ。
'Size属性だけ変えたって、パディングされるだけで、このままでは0から65535までしか入らないんですよ。
GNATの文字コード関係は、問題しかないですが、何もこんなところまで……。
規格でもWide_Characterからwchar_tに変換する関数が定義されていて、それに引きずられたのかもしれませんけれども……。
wchar_tなんて誰も使ってないんだろうね。
UTF-16で有名なのはWindowsとJavaだろうけど、これら向けならwchar_tは16ビットで問題ないし、それ以外の環境ではそもそもwchar_tなんて使う必要ないんじゃない?
君も確か、シフトJISとUnicodeの変換にmbstowcsを使おうとして、FreeBSDでは動作が違うことに気付いてiconvで直接UTF-8にすることにしたんじゃなかったっけ。
ワイド文字列型を経由しないなら関係ないよね。
確かにWindows以外でwchar_tの使いどころなんて無いですけど……。
この分じゃ永久に直らないなこれは……。
後日譚その1
後で見つけたのですが、a-assert.adsを見るに、gcc(GNAT)は、pragma Warnings (Off);で警告を抑えこみさえすればpureなpackageからでも何でも使えるようですね。
彼には黙っておきますか……。
後日譚その2
64bit環境ですと、ABIの変化から、Cの可変引数な関数は全く呼べなくなってますね。
もちろん黙っておきますけど、彼のことだから、mingw-w64とか使い出しそうで怖いなあ……。