Ada 202x (20日目) - Globalその2¶
続き。 今回理解しきれているか自信がありません。 おかしいところがありましたらご指摘ください。
これまでのあらすじ¶
翻訳単位や可視性の壁を超えてグローバル変数へのアクセスが競合していないか検証するために Global
アスペクトで表明ができるようになりました。
経緯¶
ところで、当たり前ですがこのin/outの解析は引数についても行われるわけです。
これも当たり前ですが in
パラメータに渡した実引数は読み取り(in)が、out
パラメータに渡したら書き込み(out)が行われていると見做されます。
in out
パラメータはその両方です。
元々サブプログラムのシグネチャに in
と out
が付いているため引数に関しては何もしなくても充分と思われるかもしれません。
しかしAdaでは in
パラメータに書き込むこともできます。
例えば Ada.Numerics.Float_Random
や Ada.Numerics.Discrete_Random
の Random
関数は次の乱数を求める副作用として引数 Gen
を更新します。
何の表明もなく引数を書き換えるなんて酷いですよね。
関数は副作用なしがPascal等古代言語の暗黙の規約です。
Fortranでも random_number
はサブルーチンになっています。
Adaでも2012で解禁されるまでは関数は in
のみのはずでした。
乱数ライブラリはAda 95からあります。
これは何も規格が裏技を要求しているわけではなく、Adaでは in
引数を書き換える方法がいくつかあります。
勿論キャストやメモリアクセス等で所謂危険な操作を行うこともできますが、認められた安全なやり方もあります。
問題¶
次のパッケージを実装してください。
Random
関数は呼び出しごとに異なる値を返す必要があります。
真面目に乱数を実装する必要はありません。
Address
、Export
、Import
等と標準ライブラリを含めた他のパッケージ、グローバル変数は使用禁止とします。
access型は使えるものとします。
package Float_Random is
type Generator is limited private;
function Random (Gen : Generator) return Float;
-- 呼び出しごとに異なる値を返すこと
end Float_Random;
「access型は使えるものとします」って時点で答えですけどね!
実装例1¶
メモリリーク上等。
package Float_Random is
type Generator is limited private;
function Random (Gen : Generator) return Float;
private
type Float_Access is access Float;
type Generator is limited record
State : not null Float_Access := new Float'(0.0);
end record;
end Float_Random;
package body Float_Random is
function Random (Gen : Generator) return Float is
begin
Gen.State.all := Gen.State.all + 1.0;
return Gen.State.all;
end Random;
end Float_Random;
標準ライブラリを禁止したのでこの回答例ではメモリリークさせていますが実際には Ada.Finalization.Limited_Controlled
を使ってRAIIで解放できます。
実装例2¶
Coextension。
package Float_Random is
type Generator is limited private;
function Random (Gen : Generator) return Float;
private
type Generator (State : not null access Float := new Float'(0.0)) is
limited null record;
end Float_Random;
-- bodyは上に同じ
上の改良版です。
匿名のaccess型のdiscriminantに割り当ての式(new
)を書いた場合割り当ては一体化されます。
つまりRAIIを使わなくてもメモリリークしません。
……規格上では。 残念ながらGNATにはこの機能は実装されていません。
実装例3¶
GNATでも通るやり方です。 呼ばれ方はvariable view等です。
package Float_Random is
type Generator is limited private;
function Random (Gen : Generator) return Float;
private
type Generator is limited record
Variable : not null access Generator := Generator'Unchecked_Access;
State : Float := 0.0;
end record;
end Float_Random;
package body Float_Random is
function Random (Gen : Generator) return Float is
begin
Gen.Variable.State := Gen.Variable.State + 1.0;
return Gen.Variable.State;
end Random;
end Float_Random;
最初に自分自身への書き込み可能なaccess型の値を獲得しておくことで可視性の上では変更が許されていない箇所からでも書き換えができます。
in
パラメータや access constant
を経由して可視性の上で定数として見えている箇所からも書き換えられるというだけで、実体を定数として宣言しようとするとエラーになります。
以前は実体を定数として宣言することも許されていたのですがAda 2012 Technical Corrigendum 1で禁止されました。
ですのでコンパイラによっては現時点でも変化する定数を作れます。
引数アクセスの表明¶
抽象型の場合は access
経由の書き換えとしても同じ変数の状態と見做したいですし、最後のやり方では間違いなく in
パラメータに渡された変数を書き換えています。
つまり引数の in
out
は嘘の場合があります。
ですので検証のためには本当はどうアクセスしているかを表明する必要があります。
Globalアスペクト(引数)¶
ここでも Global
アスペクトを使用します。
(引数なのに。
引数なのに。)
overriding
を使って引数の in
out
を上書きできます。
(またまた予約語を再利用している……。)
package Float_Random is
type Generator is limited private;
function Random (Gen : Generator) return Float
with Global => overriding in out Gen;
private
type Generator is limited record
Variable : not null access Generator := Generator'Unchecked_Access;
State : Float := 0.0;
end record;
end Float_Random;
これは回答例3に Global
アスペクトを書いた例です。
回答例2も(自信がありませんが恐らく)これでいいです。 しかし、実は回答例1に適用するにはこれだけでは駄目です。
Internalアスペクト/Compoundアスペクト¶
多くの言語では都合よく忘れられていることがあります。 ヒープというのはグローバル変数なのです。 ヒープをスレッドローカルなものとしてスレッド間のポインタ転送を禁止している言語もあります。
Adaでもこの性質は忘れられておらず様々な制限となっています。
例えば Pure
なパッケージでは動的にメモリ割り当て可能な access
型を宣言できません。
SPARKでは access
型のownership検証が実装されました。
Rust等で流行りのやつです。
(https://www.adacore.com/devlog にて "Mon May 13 2019" で検索すると出てきます。
来年予定のリリースノートは http://docs.adacore.com/live/wave/spark2014-release-notes/html/spark2014_release_note/release_notes_20.html にあります。
それ以前にもμSPARK等が研究用として存在していました。)
Ada 202xでも最初はownership検証が検討されていましたが、機能を制限しているSPARKと異なりフルセットのAdaに持ち込むには余りにも難易度が高い話です。
そのため、まずは簡易な access
型の所有関係だけが採択されました。
Internal
アスペクトはその access
型が他の型の部品として使われることを示します。
Compound
アスペクトは Internal
な access
型を所有することを示します。
Internal
な access
型を通じた変更は外側の Compound
型オブジェクトの変更と一体として扱われます。
木構造等入れ子の場合は Compound
はルートにのみ付けます。
回答例1に付けてみます。
package Float_Random is
type Generator is limited private;
function Random (Gen : Generator) return Float
with Global => overriding in out Gen;
private
type Float_Access is access Float
with Internal;
type Generator is limited record
State : not null Float_Access := new Float'(0.0);
end record
with Compound;
end Float_Random;
'Move¶
Internal
なaccess型は複数の変数が同じヒープオブジェクトを指すことを禁止されます。
所有者はひとつでなければならないからです。
しかし、複数から所有させる意図がなくても一時的にでも代入ができないと困るときがあります。 例えば単純なリンクリストへの追加でも困ります。
declare
type Node;
type Node_Ptr is access Node with Internal;
type Node is limited record
Element : Float;
Next : Node_Ptr;
end record;
type List is limited record
First : Node_Ptr;
end record
with Compound;
Root : List := (First => null);
begin
-- 1つ目を追加
Root.First := new Node (Element => 1.0, Next => null);
-- 2つ目を追加
declare
New_Node : not null Node_Ptr :=
new Node
(Element => 2.0,
Next => Root.First); -- error
begin
Root.First := New_Node; -- error
end;
追加の過程で一時的に2つの変数が同じヒープオブジェクトを指すことになります。
このためaccess型のオブジェクトに 'Move
属性が追加されました。
オブジェクトの値を返すと同時にそのオブジェクトを null
にします。
begin
-- 1つ目を追加
Root.First := new Node (Element => 1.0, Next => null);
-- 2つ目を追加
declare
New_Node : not null Node_Ptr :=
new Node
(Element => 2.0,
Next => Root.First'Move);
begin
Root.First := New_Node'Move;
end;
自信はありませんが恐らく同じ Compound
オブジェクト内でしたら複数から同じヒープオブジェクトを指すこともできるのではと思います。
でなければ双方向リンクリストが実装できなくなります。
匿名のaccess型への代入(変換)も許されるのではと思います。 でなければユーザー定義参照型も実装できずコンテナに支障がありますので。
関連AI¶
AI12-0240-6 Global aspect and access types used to implement Abstract Data Types