Adaの引数
Adaの引数って激ヤバだよねー。
プリミティブ型でも関数内で変数を書き換えられちゃう。
with Ada.Text_IO; use Ada.Text_IO;
with GNAT.Debug_Utilities; use GNAT.Debug_Utilities;
procedure Call is
procedure Sub (A : in Integer; B : out Integer) is
begin
Put_Line ("Sub:");
Put_Line ("A = " & Integer'Image (A) & " at " & Image (A'Address));
Put_Line ("B = " & Integer'Image (B) & " at " & Image (B'Address));
-- A := 11; Error because A is passed with "in".
B := 11;
end Sub;
A : Integer := 7;
B : Integer := 7;
begin Put_Line ("Parent:");
Put_Line ("A = " & Integer'Image (A) & " at " & Image (A'Address));
Put_Line ("B = " & Integer'Image (B) & " at " & Image (B'Address));
Sub (A, B);
Put_Line ("Changed:");
Put_Line ("A = " & Integer'Image (A) & " at " & Image (A'Address));
Put_Line ("B = " & Integer'Image (B) & " at " & Image (B'Address));
end Call;
Parent: A = 7 at 16#BFFF_F514# B = 7 at 16#BFFF_F510# Sub: A = 7 at 16#BFFF_F3F0# B = 4 at 16#BFFF_F394# Changed: A = 7 at 16#BFFF_F514# B = 11 at 16#BFFF_F510#
Elementary型はBy CopyなのでSub内のBは別のアドレスなんだけど、そこでの書き換えがちゃんと親に反映されてる。親の変数のアドレスは一定なのにも注目。反映の仕方もBy Copy
なのが分かる。
書き換えのタイミングが気になるけれど、これは副プログラム内で書き換えられた時にリアルタイムで親の変数も書き換わるのではなくて、return
と同時になる。Taskを使って検証してみるとこんな感じ。
with Ada.Text_IO; use Ada.Text_IO;
with GNAT.Debug_Utilities; use GNAT.Debug_Utilities;
procedure Out_Timing is V : Integer := 17;
task Checker is
entry Die;
end Checker;
task body Checker is
begin
loop
delay 1.0;
select
accept Die;
exit;
else
Put_Line("* Now V is " & Integer'Image (V));
end select;
end loop;
end Checker;
procedure Change (Inner_V : out Integer) is
begin
Put_Line ("Waiting for the timing checker for 2 seconds");
delay 2.0;
Put_Line ("Chainging the value of V.");
Inner_V := 11;
Put_Line ("V changed. Wating for the timing checker for 10 seconds.");
delay 2.0;
Put_Line ("Then returning.");
end Change;
begin
Put_Line ("Let's change V.");
Change (V);
Put_Line ("Change done.");
Put_Line ("Waiting for the timing checker for 2 seconds");
delay 2.0;
Checker.Die;
end Out_Timing;
Let's change V. Waiting for the timing checker for 2 seconds * Now V is 17 Chainging the value of V. V changed. Wating for the timing checker for 10 seconds. * Now V is 17 * Now V is 17 Then returning. Change done. Waiting for the timing checker for 2 seconds * Now V is 11 * Now V is 11
強調してる部分では副プログラムのInner_V
は11になってるけれど、V
は17
のまま。呼び出し元の変数に11
が反映されるのはreturn
と同時なのがわかる。アトミックな雰囲気で安心。
Composite型は状況によってはBy-Reference
で渡されるんだけど、Adaの場合参照渡しとかそういうレベルじゃない。まずは常識的な配列の要素の変更を。
with Ada.Text_IO; use Ada.Text_IO;
with GNAT.Debug_Utilities; use GNAT.Debug_Utilities;
with System;
procedure Call_Composition is
function I (I : Integer) return String renames Integer'Image;
function I (I : System.Address) return String renames Image;
type Three_Integers is array (1 .. 3) of Integer;
procedure Take_Array
-- (A : in Three_Integers) requries "out" to change the element of arrays
(A : out Three_Integers)
is begin
Put_Line ("A:" & I (A'Address));
A(2) := 17;
end Take_Array;
X : Three_Integers := (1, 3, 5);
begin Put_Line ("X:" & I (X'Address));
Take_Array (X);
Put_Line ("1 =>" & I (X (1)) & ", 2 =>" & I (X (2)) & ", 3 =>" & I (X (3)));
end Call_Composition;
X:16#BFFF_F4FC# A:16#BFFF_F4FC# 1 => 1, 2 => 17, 3 => 5
X
とY
のアドレスが同じなのでBy-Reference
で渡されているのが分かる。out
が指定されていれば渡されたComposite型の要素は書き換えが可能になる。逆にin
だと要素の書き換えも禁止されるのでとても安心。
配列自体の置換も出来るというか、Adaの場合配列の代入はコピーだから当然か。
with Ada.Text_IO; use Ada.Text_IO;
with GNAT.Debug_Utilities; use GNAT.Debug_Utilities;
with System;
procedure Call_Composition_Replace is
function I (I : Integer) return String renames Integer'Image;
function I (I : System.Address) return String renames Image;
type Three_Integers is array (1 .. 3) of Integer;
procedure Replace_Array (A : out Three_Integers) is
B : Three_Integers := (7, 17, 13);
begin
Put_Line ("A:" & I (A'Address));
Put_Line ("B:" & I (B'Address));
A := B;
Put_Line ("A:" & I (A'Address));
end Replace_Array;
X : Three_Integers := (1, 3, 5);
begin Put_Line ("X:" & I (X'Address));
Replace_Array (X);
Put_Line ("1 =>" & I (X (1)) & ", 2 =>" & I (X (2)) & ", 3=>" & I (X (3)));
Put_Line ("X:" & I (X'Address));
end Call_Composition_Replace;
X:16#BFFF_F4C0# A:16#BFFF_F4C0# B:16#BFFF_F3D4# A:16#BFFF_F4C0# 1 => 7, 2 => 17, 3=> 13 X:16#BFFF_F4C0#
これらの挙動はrecord型でも同じ。ところで、GNATの場合、おそらくレジスタに値が乗るぐらいオブジェクトが小さい場合はBy Copyで渡してくれるらしい。これは効率がいいんだけど、コピーされてると変更結果がちゃんと反映されるのか心配になる。
vwith Ada.Text_IO; use Ada.Text_IO;
with GNAT.Debug_Utilities; use GNAT.Debug_Utilities;
with System;
procedure Call_Record is
function I (I : Integer) return String renames Integer'Image;
function I (I : System.Address) return String renames Image;
-- enough small to pass by reference
type Something is
Record
Value : Integer;
end record;
-- bigger record
type Something_Big is
record
value : Integer;
Value_2 : Integer;
Value_3 : Integer;
end record;
procedure Change_Something (A : out Something) is
begin
Put_Line ("A:" & I (A'Address));
A.Value := 17;
end Change_Something;
procedure Change_Something_Big (B : out Something_Big) is
begin
Put_Line ("B:" & I (B'Address));
B.Value := 17;
end Change_Something_Big;
X : Something := (Value => 11);
Y : Something_Big := (Value => 11, Others => 0);
begin Put_Line ("X:" & I (X'Address));
Change_Something (X);
Put_Line (I (X.Value));
Put_Line ("X:" & I (X'Address));
New_Line;
Put_Line ("Y:" & I (Y'Address));
Change_Something_Big (Y);
Put_Line (I (Y.Value));
Put_Line ("Y:" & I (Y'Address));
end Call_Record;
X:16#BFFF_F510# A:16#BFFF_F490# 17 X:16#BFFF_F510# Y:16#BFFF_F504# B:16#BFFF_F504# 17 XY:16#BFFF_F504#
X
とA
はメモリ上では違うオブジェクトなのに、きちんと変更されている。すごい。
こんな感じでそもそも値渡しなのか参照渡し(と言っていいのかな)なのかが一定しないのがAdaのヤバいところ。が、これはあくまでもコンパイラが考えることであって、プログラマは気にしなくても平気。とにかく、どんなタイプの変数でも投げられるし、投げた先で書き換えればそれがちゃんと親にも反映されるってことが確実ってだけ。
さて、ここまでは自動変数の話だったんど、access
だとどうだろう。access
には参照が入っていて、By Copy
でそれを渡すことになる。これはJavaと同じ挙動だけれど、Adaの場合はひと味違う。具体的に言うと副プログラム内でオブジェクトの置き換えも出来てしまう。
with Ada.Text_IO; use Ada.Text_IO;
with GNAT.Debug_Utilities; use GNAT.Debug_Utilities;
with System;
procedure Call_Access is
function I (I : Integer) return String renames Integer'Image;
function I (I : System.Address) return String renames Image;
type Something is
Record
Value : Integer;
end record;
type Something_Access is access Something;
procedure Change_Something (A : in
Something_Access) is
begin
-- allowed even if passed with "in" for access type
A.Value := 17;
-- following is not allowed because "A" is "in".
-- A := new Something'(Value => 11);
end Change_Something;
procedure Replace_Something (A : out not null Something_Access) is
B : Something_Access := new Something'(Value => 13);
begin
Put_Line ("Before:");
Put_Line ("A:" & I (A'Address));
Put_Line ("A.all:" & I (A.all'Address));
Put_Line ("B:" & I (B'Address));
Put_Line ("B.all:" & I (B.all'Address));
A := B;
Put_Line ("After:");
Put_Line ("A:" & I (A'Address));
Put_Line ("A.all:" & I (A.all'Address));
end Replace_Something;
X : Something_Access := new Something'(Value => 11);
begin
-- This is basic process, possible with Java.
Change_Something (X);
Put_Line (I (X.Value)); New_Line;
-- This shows you why you should Ada.
Put_Line ("X:" & I (X'Address));
Put_Line ("X.all:" & I (X.all'Address));
Replace_Something (X);
Put_Line ("X:" & I (X'Address));
Put_Line (I (X.Value));
Put_Line ("X.all:" & I (X.all'Address));
end Call_Access;
17 X:16#BFFF_F510# X.all:16#0010_00F0# Before: A:16#BFFF_F4A0# A.all:16#0010_00F0# B:16#BFFF_F424# B.all:16#0010_0100# After: A:16#BFFF_F4A0# A.all:16#0010_0100# X:16#BFFF_F510# X.all:16#0010_0100# 13
これはやばい。
最初のChange_Something
でなぜかin
なのに要素の変更が出来てしまってるけれど、これは妥当。どうせ他の変数から同じオブジェクトを参照させれば引数部分の制限は関係ないし。ちなみに、Cだと->
使う状況だけど、Adaは書き分けなくていいので楽ちんかな。もちろん、参照値自体の置き換えはin
だと制限される。
そんな事より、Replace_Something
がすごい。副プログラム内でオブジェクトの入れ替えが出来てる。Javaだとこれが出来ないんだなー。
そんな訳で、Adaの引数はとてもすごくて、外見は一貫して参照渡しっぽい雰囲気なのに、内部ではコピーしたり参照を渡してたりと勝手に最適化してくれちゃうのだ。プログラマは中で何が起きてるのか気にせずに、一環したコードが書けるのがとてもいい。プリミティブとかオブジェクトとかも気にしなくていい。
他の言語を見てみると、JavaとCは論外として、C#はref
とか使えば参照渡しが出来る。でもこれがプログラムとして一貫しているかと言われると微妙なところだと思う。その点、Adaの引数はとてもヤバいですね。