Ada 202x (26日目) - 契約と継承

一番理解も説明も難しいところが来てしまいました……。 怪しい箇所がありましたらご指摘ください。 遠慮なく。 ええもう本当に。

リスコフの置換原則

継承で大事なことはリスコフの置換原則(Liskov substitution principle、LSP)を守ることです。 基底型の性質は全て派生型で満たされなければならない(置換可能性)というやつです。

しかし実際にこれを守るのは非常に難しいです。 理解するのも問題のないように言語仕様を制定するのもコードを書くのも難しいです。 Adaでも細部の仕様はAda 2012、Technical Corrigendum 1、そしてAda 202xと二転三転した部分もあります。

これから説明するものは他に契約プログラミングを実装した言語と挙動が異なる部分も多いです。 標準化委員会が長年に渡って詰めた結果であり、違和感を感じても熟考していただければと思います。 (正直私も違和感がある部分がなくはないです。)

そして繰り返しになりますが私の理解がおかしければご指摘いただければ幸いです。

Walter先生が深く考えずに実装して後悔しているらしい(?)D言語の挙動はまあ参考にしなくていいとしまして、元祖のEiffelとも異なる部分があります。

そのような事情ですので今回はAda 202xでここが変わったとピンポイントに書くのではなく、全体を見渡す形で説明したいと思います。

class-wide アスペクト

契約関係のアスペクトには後ろに 'Class の付いた版があります。 tagged型とそのプリミティブ(同じパッケージで宣言されたサブプログラム、多態の対象になる)にだけ指定できます。

契約が導入される前のAda 2005以前にもストリームにclass-wide型の値を実際の型の情報付きで読み書きするための T'Class'Read/T'Class'Write 属性がありました。 同じように 'Class の付いた版のアスペクトは(基本的に)派生先にも継承されます。 (以降は「'Class の付いた版」のことを「class-wide版」と書きます。)

逆に言えば 'Class の付かない版のアスペクトは(基本的に)その型限りということです。 とはいえアスペクトが継承されなくてもアスペクトによって起こる動作そのものは継承されますから非常にややこしいです。

Global'ClassType_Invariant'ClassPre'ClassPost'Class そして Stable_Properties'Class が存在します。

ただし Stable_Properties'Class は継承されません。 後述します。

ですので実際に考える必要があるのは他の4つとなります。

サンプル

サンプルのベースを先に書いておきます。 これに各アスペクトを追加していきます。

package Base is
   type B is tagged limited private;
   function Create (Value : Integer) return B;
   function Get_Value (Object : B) return Integer;
   procedure Set_Value (Object : in out B; Value : in Integer);
   Time : Integer := 0;
   Set_Value_Is_Canceled : exception;
private
   type B is tagged limited record
      E1 : Ineger := 0;
      Modified_Time : Integer := Time;
   end record;
end Base;
package body Base is

   function Create (Value : Integer) return B is
   begin
      return B'(E1 => Value, Modified_Time => Time);
   end Create;

   function Get_Value (Object : B) return Integer is
   begin
      return Object.E1;
   end Get_Value;

   procedure Set_Value (Object : in out B; Value : in Integer);
   begin
      Object.E1 := Value;
      Modified_Time := Time;
   end Set_Value;

end Base;
with Base;
package Derived is
   type D is new Base with private;
   overriding function Create (Value : Integer) return D;
   overriding function Get_Value (Object : D) return Integer;
   overriding procedure Set_Value (Object : in out D; Value : in Integer);
private
   type D is new Base with record
      E2 : Ineger := 0;
   end record;
end Derived;
package body Derived is

   overriding function Create (Value : Integer) return D is
   begin
      return D'(Base.Create (0) with E2 => Value);
   end Create;

   overriding function Get_Value (Object : D) return Integer is
   begin
      return Object.E2;
   end Get_Value;

   overriding procedure Set_Value (Object : in out D; Value : in Integer) is
   begin
      Object.E2 := Value;
   end Set_Value;

end Derived;

Globalと継承

Global はサブプログラムがアクセスするグローバル変数を表明します。 継承されません。

例に追加してみます。

package Base is
   -- 省略
   function Create (Value : Integer) return B
     with Global => in Time;
   function Get_Value (Object : B) return Integer
     with Global => null;
   procedure Set_Value (Object : in out B; Value : in Integer)
     with Global => in Time;
   -- 省略
with Base;
package Derived is
   -- 省略
   overriding function Create (Value : Integer) return D
     with Global => in Time;
   overriding function Get_Value (Object : D) return Integer
     with Global => null;
   overriding procedure Set_Value (Object : in out D; Value : in Integer)
     with Global => null;
   -- 省略

Base.Create とそれを呼び出している Derived.Create 、そして Base.Set_ValueTimein アクセスする、それ以外はグローバル変数にはアクセスしない、と、そのまま書きました。

これはそれぞれ普通に呼んでいる分には問題ありません。

with Base;
with Derived;
procedure Main is
begin
   parallel do
      declare
         B_Object : Base.B :=
           Base.Create (1);
             -- Global => in Time
      begin
         Base.Set_Value (B_Object);
           -- Global => in Time
      end;
   and
      declare
         D_Object : Derived.D :=
           Derived.Create (2);
             -- Global => in Time
      begin
         Base.Set_Value (D_Object);
           -- Global => null
      end;
   end do;
end Main;

しかし、このままでは多態呼び出しができなくなります。

with Base;
with Derived;
procedure Main is
begin
   parallel do
      declare
         B_As_Classwide_B : Base.B'Class :=
           Base.Create (1);
             -- Global => in Time
      begin
         Base.Set_Value (B_As_Classwide_B);
           -- Global => in out all
      end;
   and
      declare
         D_As_Classwide_B : Base.B'Class :=
           Derived.Create (2);
             -- Global => in Time
      begin
         Base.Set_Value (D_As_Classwide_B); -- error
           -- Global => in out all
      end;
   end do;
end Main;

多態呼び出しにおいて派生先ではどのような変数にアクセスしているかわかりませんので in out all が仮定されてしまいます。

これを解決するためには基底の側に Global'Class も併せて指定します。

package Base is
   -- 省略
   function Create (Value : Integer) return B
     with
       Global       => in Time,
       Global'Class => in Time;
   function Get_Value (Object : B) return Integer
     with
       Global       => null,
       Global'Class => null;
   procedure Set_Value (Object : in out B; Value : in Integer)
     with
       Global       => in Time,
       Global'Class => in Time;
   -- 省略
with Base;
with Derived;
procedure Main is
begin
   parallel do
      declare
         B_As_Classwide_B : Base.B'Class :=
           Base.Create (1);
             -- Global => in Time
      begin
         Base.Set_Value (B_As_Classwide_B);
           -- Global => in Time
      end;
   and
      declare
         D_As_Classwide_B : Base.B'Class :=
            Derived.Create (2);
              -- Global => in Time
      begin
         Base.Set_Value (D_As_Classwide_B);
           -- Global => in Time
      end;
   end do;
end Main;

in アクセス同士は衝突しませんので通るようになりました。

Global'Classoverriding では変更できません。 そして GlobalGlobal'Class の部分集合である必要があります。

デフォルトの in out all は全ての上位集合ですので指定しなくても通常は問題ありませんが、例のように並列化の中で多態呼び出しに制限ができてしまいます。

Type_Invariantと継承

Type_Invariant はその型のオブジェクトが常に満たさなければならない不変条件です。 継承されませんが、派生型に埋め込まれている基底型部分では動作します。

基底型 Base.B に適当な Type_Invariant を追加してみます。

package Base is
   type B is tagged limited private
     with Type_Invariant => Get_Value (B) > 0;
   -- 省略

この不変条件は、この例の場合ですとデフォルト初期化及び Base.CreateBase.Set_Value の最後でチェックされます。

Derived.D のオブジェクトに影響するのは埋め込まれている B 部分だけです。 具体的には Derived.Create の中で Base.Create を呼んでいる部分だけですね。

Derived.Create を呼び出したら何が起きるかを見てみます。

with Derived;
procedure Main is
   Object : Derived.D := Derived.Create (1); -- runtime error
     -- Type_Invariant => Base.Get_Value (Base.B (Object)) > 0
begin
   null;
end Main;

引数は 1 ですが Derived.Create の中では Base.Create には 0 を渡しているため Object.E10 になります。 Get_ValueDerivedoverriding していますが不変条件の式中では多態しません。 結果 Get_ValueObject.E1 を返して不変条件違反で実行時エラーになります。 (多態してしまいますと条件自体を後から変えられることになってしまいます。 この例ですと E10 のインスタンスを作れてしまうことになります。)

Type_Invariant'Class は継承され、派生型の文脈で再解釈されます。

先程の不変条件を Type_Invariant'Class に変えてみます。

package Base is
   type B is tagged limited private
     with Type_Invariant'Class => Get_Value (B) > 0;
   -- 省略

これは、次のように両方に Type_Invariant を指定したのと同じ動作になります。

package Base is
   type B is tagged limited private
     with Type_Invariant => Get_Value (B) > 0;
   -- 省略
with Base;
package Derived is
   type D is new Base with private
     with Type_Invariant => Get_Value (D) > 0;
   -- 省略

Base.B の方では Base.Get_Value が呼ばれ、 Derived の方では Derived.Get_Value が呼ばれることになります。 最終的に D のオブジェクトに対するチェックは Base.Get_Value (Base.B (Object)) > 0 and Derived.Get_Value (Object) > 0 となります。

この場合もやはり実行時エラーになります。

class-wide版の条件内の関数呼び出しは多態せずに両方がチェックされます。 継承階層の途中にabstract型(抽象型ではなくてインスタンスを作れない方)がある場合はabstract型のところだけ飛ばされます。 これは事前条件 Pre'Class でも事後条件 Post'Class でも同様です。

元々はclass-wide版の方は式中の関数呼び出しは多態していたのですがTechnical Corrigendum 1で変更されました。 Ada 202xでabstract型は飛ばすようになりました。

どうしても多態したければ、明示的に書けば多態させることもできます。 Get_Value (B'Class (B)) > 0 みたいにclass-wide型にすればいいです。

Preと継承

Pre はサブプログラムの事前条件です。

継承されませんが、該当サブプログラムが呼び出されれば動作します。

事実上サブプログラムの先頭に埋め込まれるpragma Assert です。 他とは無関係に評価されます。 overriding する際に上手く再定義しないとLSPを満たせなくなります。

Derived.Set_Value に適当な Pre を追加してみます。 (なお Type_Invariant の例で追加した不変条件は消しておきます。)

with Base;
package Derived is
   -- 省略
   overriding procedure Set_Value (Object : in out D; Value : in Integer)
     with Pre => Value = 0;
   -- 省略

Base.Set_Value には事前条件を書いていないため常に成立する True を仮定するのが妥当です。 そして事前条件は緩めることしか許されていませんが、この Value = 0 はそのルールを破っています。

というわけで overriding 先に Pre を使うのは推奨できません。 推奨できないだけで動作を把握していれば色々便利な使い方もありますが……

Pre'Class は継承され、派生型の文脈で再解釈されます。

先ほどの事前条件を Pre'Class に変えてみます。

with Base;
package Derived is
   -- 省略
   overriding procedure Set_Value (Object : in out D; Value : in Integer)
     with Pre'Class => Value = 0; -- error
   -- 省略

これはコンパイルエラーになります。 Technical Corrigendum 1からエラーということになり、元の Base.Set_Value にも Pre'Class が必要になりました。 何も書いていない場合は True を仮定するのが妥当ですが、それで嬉しい場合はまずないですし。 (原則から言えば先程の Pre も同様にコンパイルエラーでいいはずですが「色々便利な使い方」のために見逃されています。)

というわけで Base.Set_Value にも Pre'Class を書き足します。

package Base is
   -- 省略
   procedure Set_Value (Object : in out B; Value : in Integer)
     with Pre'Class => Value > 0;
   -- 省略

Pre'Class は条件を緩めることしかできないというルールに沿うため継承したものと論理和が取られます。

ですので Derived.Set_Value の事前条件は最終的に Value > 0 or Value = 0 となります。

with Base;
with Derived;
procedure Main is
begin
   Base.Set_Value (Base.Create (1), 100);
     -- Pre => Value > 0
   Derived.Set_Value (Derived.Create (2), 0);
     -- Pre => Value > 0 or Value = 0
end Main;

注意しないといけないのは事前条件は多態しないということです。

Type_Invariant で触れましたように事前条件の式の中での呼び出しは多態しません。 それとは別に、事前条件の書かれているサブプログラム自体を多態呼び出ししても事前条件は多態しません。

Set_Value を多態呼び出ししてみます。

with Base;
with Derived;
procedure Main is
   D_As_Classwide_B : Base.B'Class := Derived.Create (2);
begin
   Base.Set_Value (D_As_Classwide_B, 0); -- runtime error
     -- Pre => Value > 0
end Main;

この場合 Base.Set_ValuePre'Class である Value > 0 のみが評価されて実行時エラーになります。

確かに派生先では事前条件を緩めることができますが、アップキャストして基底型として扱っているときは基底型の条件に沿わないとLSPを満たせないからです。

これに関して詳しく説明しようとしますと文章量が必要になりますし、私が説明するよりもAARMの該当箇所を読んでいただいたほうがいいかなと……。

……わかんないですよねー。 私もわかりません。 規格の文面なんて前提知識がないと意味不明です。

えーと、D言語のコンパイラdmdに対するバグ報告からの議論を読んでいただいたほうがわかりやすいと思います。 Meyer先生の「オブジェクト指向入門」の該当箇所も示されています。

ちなみにGNATにも同じバグがありましたがgcc 8ぐらいで直りました。 dmdはまだ直ってないようです。

Postと継承

Post はサブプログラムの事後条件です。

継承されませんが、該当サブプログラムが呼び出されれば動作します。

事実上サブプログラムの最後に埋め込まれるpragma Assert です。 呼び出し側にとっては表明されていない隠された事後条件みたいに考えればよいと思います。 実際の返値の値域は表明されたものよりも狭いのですが、呼び出し側はそれに依存することはできない、みたいな。 Pre と異なりLSPの上で問題はないため使っていいです。

Base.Set_Value に適当な Post を追加してみます。

package Base is
   -- 省略
   procedure Set_Value (Object : in out B; Value : in Integer)
     with
       Pre'Class => Value > 0,
       Post      => Get_Value (Object) > 0;
   -- 省略

Derived.Set_Value の事前条件には(Pre'Class の例で追加した) Pre'Class => Value = 0 があるものとします。

with Base;
package Derived is
   -- 省略
   overriding procedure Set_Value (Object : in out D; Value : in Integer)
     with Pre'Class => Value = 0;
   -- 省略

そうしますと Derived.Set_Value (Object, 0) の呼び出しの後は Base.Get_Value (Base.B (Object))Derived.Get_Value (Object) の両方とも結果が 0 になりますが Base.Set_ValuePostDerived.Set_Value には継承されませんので問題にはなりません。

with Base;
with Derived;
procedure Main is
begin
   Base.Set_Value (Base.Create (1), 100);
     -- Pre  => Value > 0
     -- Post => Base.Get_Value (Object) > 0
   Derived.Set_Value (Derived.Create (2), 0);
     -- Pre  => Value > 0 or Value = 0
     -- Post => True
end Main;

Post'Class は継承され、派生型の文脈で再解釈されます。

先程の事後条件を Post'Class に変更してみます。

package Base is
   -- 省略
   procedure Set_Value (Object : in out B; Value : in Integer)
     with
       Pre'Class  => Value > 0,
       Post'Class => Get_Value (Object) > 0;
   -- 省略

この Post'ClassDerived.Set_Value に継承されます。

表明されている事後条件はきつくすることしかできないというルールに沿うため継承したものと論理積が取られます。

with Base;
with Derived;
procedure Main is
begin
   Base.Set_Value (Base.Create (1), 100);
     -- Pre  => Value > 0
     -- Post => Base.Get_Value (Object) > 0
   Derived.Set_Value (Derived.Create (2), 0); -- runtime error
     -- Pre  => Value > 0 or Value = 0
     -- Post => Base.Get_Value (Object) > 0
     --           and Derived.Get_Value (Object) > 0
end Main;

結果、実行時エラーになってしまいました。

元で表明されている事後条件には全ての派生型で考えられる最も緩い条件を指定しなければなりません。

ですのでここでは Base.Set_ValuePost'Class を修正します。

package Base is
   -- 省略
   procedure Set_Value (Object : in out B; Value : in Integer)
     with
       Pre'Class  => Value > 0,
       Post'Class => Get_Value (Object) >= 0;
   -- 省略
with Base;
with Derived;
procedure Main is
begin
   Base.Set_Value (Base.Create (1), 100);
     -- Pre  => Value > 0
     -- Post => Base.Get_Value (Object) >= 0
   Derived.Set_Value (Derived.Create (2), 0);
     -- Pre  => Value > 0 or Value = 0
     -- Post => Base.Get_Value (Object) >= 0
     --           and Derived.Get_Value (Object) >= 0
end Main;

事後条件は多態します。

Type_Invariant で触れましたように事後条件の式の中での呼び出しは多態しません。 それとは別に、事後条件の書かれているサブプログラム自体を多態呼び出しした場合は事後条件も多態します。

事前条件を満たすのが呼び出し側の責任なのに対して、事後条件を満たすのはサブプログラム側の責任でありそれはディスパッチ後のサブプログラム本体に付随するものだからです。

事前条件も事後条件もどちらもきつい方を満たさないといけないという覚え方でいいと思います。 (雑。)

上記以外で影響するアスペクト

Stable_Properties も含めて軽く触れていきます。

Stable_Propertiesと継承

Stable_Properties は各プリミティブの Post として展開され、派生先には継承されません。

Stable_Properties'Class は各プリミティブの Post'Class として展開され、派生先には継承されません。 展開された Post'Class 自体は派生先に継承されます。 ややこしいです。

このような仕様ですので Stable_Properties'Class を使っている基底型から派生した際、追加したプリミティブに基底型から継承したプリミティブと同様の事後条件を与えるためには Stable_Properties'Class を繰り返し指定する必要があります。

仮に継承したら、あるいは同じものを指定したらしたで、同じ条件が何度も重複して追加されていく形になって非効率になる恐れがあります。 継承されない仕様になった理由だそうです。

練り込み不足を感じます……。

Default_Initial_Conditionと継承

デフォルト初期化の事後条件 Default_Initial_Condition にはclass-wide版はありません。

継承はされませんが派生型のオブジェクトに埋め込まれている基底型部分では動作します。 aggregate式の基底型部分等も含めてもしデフォルト初期化を使ったら基底型の Default_Initial_Condition がチェックされますし、基底型部分にデフォルト初期化を使わなければ関係ないです。

例えば Base.B に常に失敗する Default_Initial_Condition を書き足したとします。

package Base is
   type B is tagged limited private
     with Default_Initial_Condition => False;

しかし Derived.Create は基底部分は Base.Create を使って作成していますので関係ないです。

package body Derived is

   overriding function Create (Value : Integer) return D is
   begin
      return D'(Base.Create (0) with E2 => Value);
   end Create;

   -- 省略

次のように書き換えますと Base.B のデフォルト初期化となり Default_Initial_Condition もチェックされます。 (多分。 自信ないです。)

package body Derived is

   overriding function Create (Value : Integer) return D is
   begin
      return D'(Base.B with E2 => Value); -- runtime error
   end Create;

   -- 省略

Nonblockingと継承

サブプログラムやRAIIがブロッキングするかどうかを指定する Nonblocking にはclass-wide版はありませんが、派生先に継承されます。

overriding によって Nonblocking かどうかを変えることはできません。

Static_Predicate​/Dynamic_Predicateと継承

Type_Invariant と同様に動作します。

…… Dynamic_PredicateType_Invariant って何が違うのでしょうか。

……えーと、えーと、そうです。 Dynamic_Predicatesubtype にも追加できます。 ……えーと、えーと、他には……。

不変条件はサブプログラムの出口でチェックされます。 predicateは型変換や引数等の入り口側でのチェックが主になります。

関連AI

  • AI12-0170-1 Abstract subprogram calls in class-wide precondition expressions

  • AI12-0195-1 Inheriting body but overriding precondition or postcondition

  • AI12-0199-1 Abstract subprogram calls in class-wide invariant expressions

Technical Corrigendum 1で適用済みのものは含めていません。

所感

あたまいたい。

もっと自然な例を用意したい。

明らかに分量配分がおかしい。

ともかく、これで最大の難所は越えたはず……。

Pre は理屈の上では問題ありですが実用上は overridingPre が一緒になっているとまずいと気付けそうですので気にしなくていいのではと思います。

Stable_Properties'Class は普通にプロパティを宣言するようなイメージで使えれば良かったのですが。 EVEのmodelは多分そんな感じで使えますよね。 まあAdaで何層も継承するプログラムを書くほうがどうかしてるとは言えるはず? よく言われますように継承階層はabstract型や interface と実装との2層に留めましょう。 generic を多用しましょう。