Object-oriented Programming
What is Object Oriented Programming?
Object-oriented software construction is the building of software systems as structured collections of [...] abstract data type implementations.
Bertrand Meyer, “Object Oriented Software Construction”
Object Oriented Programming is about:
isolating clients from implementation details (abstraction)
isolating clients from the choice of data types (dynamic dispatching)
Object Oriented Programming is not:
the same as prototype programming (class and objects)
the same as scoping (class as the scope for methods)
the same as code reuse (use a component in a record in SPARK)
Prototypes and Scopes in SPARK
Types in SPARK come with methods aka primitive operations
package Show_Type_Primitives is type Int is range 1 .. 10; function Equal (Arg1, Arg2 : Int) return Boolean; procedure Bump (Arg : in out Int); type Approx_Int is new Int; -- implicit definition of Equal and Bump for Approx_Int end Show_Type_Primitives;
Scope for the prototype is current declarative region
... or up to the first freezing point – point at which the type must be fully defined, e.g. when defining an object of the type
OOP without dynamic dispatching = Abstract Data Types
Classes in SPARK
Classes in SPARK are tagged records
package Show_Classes is type Int is tagged record Min, Max, Value : Integer; end record; function Equal (Arg1, Arg2 : Int) return Boolean; procedure Bump (Arg : in out Int); type Approx_Int is new Int with record Precision : Natural; end record; -- implicit definition of Equal and Bump for Approx_Int end Show_Classes;
Derived types are specializations of the root type
typically with more components
inheriting the methods on the parent type
can add their own methods
Methods in SPARK
Derived methods can be overriding or not
package Show_Derived_Methods is pragma Elaborate_Body; type Int is tagged record Min, Max, Value : Integer := 0; end record; function Equal (Arg1, Arg2 : Int) return Boolean; procedure Bump (Arg : in out Int); type Approx_Int is new Int with record Precision : Natural := 0; end record; overriding function Equal (Arg1, Arg2 : Approx_Int) return Boolean; overriding procedure Bump (Arg : in out Approx_Int); not overriding procedure Blur (Arg : in out Approx_Int); end Show_Derived_Methods;package body Show_Derived_Methods is function Equal (Arg1, Arg2 : Int) return Boolean is (Arg1 = Arg2); procedure Bump (Arg : in out Int) is Next : constant Integer := (if Arg.Value < Integer'Last then Arg.Value + 1 else Integer'Last); begin if Next <= Arg.Max then Arg.Value := Next; end if; end Bump; overriding function Equal (Arg1, Arg2 : Approx_Int) return Boolean is (Arg1 = Arg2); overriding procedure Bump (Arg : in out Approx_Int) is begin Bump (Int (Arg)); end Bump; not overriding procedure Blur (Arg : in out Approx_Int) is Prev : constant Integer := (if Arg.Value > Integer'First then Arg.Value - 1 else Integer'First); begin if Arg.Value >= Prev then Arg.Value := Prev; end if; end Blur; end Show_Derived_Methods;
Method called depends on static type
with Show_Derived_Methods; use Show_Derived_Methods; procedure Use_Derived_Methods is I : Int; AI : Approx_Int; begin Bump (I); -- call to Int.Bump I.Bump; -- call to Int.Bump (object.method notation) Bump (AI); -- call to Approx_Int.Bump Bump (Int (AI)); -- call to Int.Bump end Use_Derived_Methods;
Dynamic dispatching in SPARK
Class-wide types
type of object that triggers dispatching
method called depends on dynamic type
with Show_Derived_Methods; use Show_Derived_Methods; procedure Use_Dynamic_Dispatching is I : Int; AI : Approx_Int; begin declare IC : Int'Class := Int'Class (I); begin IC.Bump; -- call to Int.Bump end; declare IC : Int'Class := Int'Class (AI); begin IC.Bump; -- call to Approx_Int.Bump end; end Use_Dynamic_Dispatching;
Class-wide views of objects
in Ada, usually manipulated through pointers
in SPARK, manipulated through parameter passing
with Show_Derived_Methods; use Show_Derived_Methods; procedure Use_Classwide_Dispatching is procedure Call_Bump (Arg : in out Int'Class) is begin Arg.Bump; end Call_Bump; I : Int; AI : Approx_Int; begin Call_Bump (Int'Class (I)); -- calls Int.Bump(I) Call_Bump (Int'Class (AI)); -- calls Approx_Int.Bump(AI) end Use_Classwide_Dispatching;
A trivial example
what is called here?
procedure Show_Trivial_Example is package Pkg_Trivial is type Int is tagged record Min, Max, Value : Integer; end record; procedure Bump (Arg : in out Int) is null; end Pkg_Trivial; use Pkg_Trivial; procedure Call_Bump (Arg : in out Int'Class) is begin Arg.Bump; end Call_Bump; begin null; end Show_Trivial_Example;
The problems with dynamic dispatching
Control and data flow are not known statically
control flow – which subprogram is called when dispatching
data flow – what data this subprogram is accessing
similar to callbacks through subprogram pointers
Avionics standard DO-178C lists 3 verification options
run all tests on parent type where derived type is used instead
cover all possible methods at dispatching calls
prove type substitutability (Liskov Substitution Principle aka LSP)
LSP – the SPARK solution to dynamic dispatching problems
Class-wide contracts on methods
Pre'Class
specifies strongest precondition for the hierarchyPost'Class
specifies weakest postcondition for the hierarchy
package Show_LSP is type Int is tagged record Min, Max, Value : Integer := 0; end record; procedure Bump (Arg : in out Int) with Pre'Class => Arg.Value < Arg.Max - 10, Post'Class => Arg.Value > Arg.Value'Old; type Approx_Int is new Int with record Precision : Natural := 0; end record; overriding procedure Bump (Arg : in out Approx_Int) with Pre'Class => Arg.Value > 100, Post'Class => Arg.Value = Arg.Value'Old; end Show_LSP;
package Show_LSP is type Int is tagged record Min, Max, Value : Integer := 0; end record; procedure Bump (Arg : in out Int) with Pre'Class => Arg.Value < Arg.Max - 10, Post'Class => Arg.Value > Arg.Value'Old; type Approx_Int is new Int with record Precision : Natural := 0; end record; overriding procedure Bump (Arg : in out Approx_Int) with Pre'Class => True, Post'Class => Arg.Value = Arg.Value'Old + 10; end Show_LSP;
package Show_LSP is type Int is tagged record Min, Max, Value : Integer := 0; end record; procedure Bump (Arg : in out Int) with Pre'Class => Arg.Value < Arg.Max - 10, Post'Class => Arg.Value > Arg.Value'Old; type Approx_Int is new Int with record Precision : Natural := 0; end record; overriding procedure Bump (Arg : in out Approx_Int); -- inherited Pre'Class from Int.Bump -- inherited Post'Class from Int.Bump end Show_LSP;
Verification of dynamic dispatching calls
Class-wide contracts used for dynamic dispatching calls
with Show_LSP; use Show_LSP; procedure Show_Dynamic_Dispatching_Verification is procedure Call_Bump (Arg : in out Int'Class) with Pre => Arg.Value < Arg.Max - 10, Post => Arg.Value > Arg.Value'Old is begin Arg.Bump; end Call_Bump; begin null; end Show_Dynamic_Dispatching_Verification;
LSP applies to data dependencies too
overriding method cannot read more global variables
overriding method cannot write more global variables
overriding method cannot have new input-output flows
SPARK RM defines
Global'Class
andDepends'Class
(not yet implemented ⟶ useGlobal
andDepends
instead)
Class-wide contracts and data abstraction
Abstraction can be used in class-wide contracts
Typically use expression functions for abstraction
package Show_Classwide_Contracts is type Int is tagged private; function Get_Value (Arg : Int) return Integer; function Small (Arg : Int) return Boolean with Ghost; procedure Bump (Arg : in out Int) with Pre'Class => Arg.Small, Post'Class => Arg.Get_Value > Arg.Get_Value'Old; private type Int is tagged record Min, Max, Value : Integer := 0; end record; function Get_Value (Arg : Int) return Integer is (Arg.Value); function Small (Arg : Int) return Boolean is (Arg.Value < Arg.Max - 10); end Show_Classwide_Contracts;
Class-wide contracts, data abstraction and overriding
Abstraction functions can be overridden freely
overriding needs not be weaker or stronger than overridden
package Show_Contract_Override is type Int is tagged record Min, Max, Value : Integer := 0; end record; function Small (Arg : Int) return Boolean is (Arg.Value < Arg.Max - 10); type Approx_Int is new Int with record Precision : Natural := 0; end record; overriding function Small (Arg : Approx_Int) return Boolean is (True); end Show_Contract_Override;
package Show_Contract_Override is type Int is tagged record Min, Max, Value : Integer := 0; end record; function Small (Arg : Int) return Boolean is (Arg.Value < Arg.Max - 10); type Approx_Int is new Int with record Precision : Natural := 0; end record; function Small (Arg : Approx_Int) return Boolean is (Arg.Value in 1 .. 100); end Show_Contract_Override;
Inherited contract reinterpreted for derived class
package Show_Contract_Override is type Int is tagged record Min, Max, Value : Integer := 0; end record; procedure Bump (Arg : in out Int) with Pre'Class => Arg.Value < Arg.Max - 10, Post'Class => Arg.Value > Arg.Value'Old; type Approx_Int is new Int with record Precision : Natural := 0; end record; overriding procedure Bump (Arg : in out Approx_Int); -- inherited Pre'Class uses Approx_Int.Small -- inherited Post'Class uses Approx_Int.Get_Value end Show_Contract_Override;
Dynamic semantics of class-wide contracts
Class-wide precondition is the disjunction (or) of
own class-wide precondition, and
class-wide preconditions of all overridden methods
Class-wide postcondition is the conjunction (and) of
own class-wide postcondition, and
class-wide postconditions of all overridden methods
Plain
Post
+ class-widePre
/Post
can be used togetherProof guarantees no violation of contracts at runtime
LSP guarantees stronger than dynamic semantics
Redispatching and Extensions_Visible aspect
Redispatching is dispatching after class-wide conversion
formal parameter cannot be converted to class-wide type when
Extensions_Visible
isFalse
with Show_Contract_Override; use Show_Contract_Override; procedure Show_Redispatching is procedure Re_Call_Bump (Arg : in out Int) is begin Int'Class (Arg).Bump; end Re_Call_Bump; begin null; end Show_Redispatching;
Aspect
Extensions_Visible
allows class-wide conversionparameter mode used also for hidden components
with Show_Contract_Override; use Show_Contract_Override; procedure Show_Redispatching is procedure Re_Call_Bump (Arg : in out Int) with Extensions_Visible is begin Int'Class (Arg).Bump; end Re_Call_Bump; begin null; end Show_Redispatching;
Code Examples / Pitfalls
Example #1
package OO_Example_01 is type Int is record Min, Max, Value : Integer; end record; procedure Bump (Arg : in out Int) with Pre'Class => Arg.Value < Arg.Max - 10, Post'Class => Arg.Value > Arg.Value'Old; end OO_Example_01;
This code is not correct. Class-wide contracts are only allowed on tagged records.
Example #2
package OO_Example_02 is type Int is tagged record Min, Max, Value : Integer; end record; procedure Bump (Arg : in out Int) with Pre => Arg.Value < Arg.Max - 10, Post => Arg.Value > Arg.Value'Old; end OO_Example_02;
This code is not correct. Plain precondition on dispatching subprogram is not allowed in SPARK. Otherwise it would have to be both weaker and stronger than the class-wide precondition (because they are both checked dynamically on both plain calls and dispatching calls).
Plain postcondition is allowed, and should be stronger than class-wide postcondition (plain postcondition used for plain calls).
Example #3
package OO_Example_03 is pragma Elaborate_Body; type Int is tagged record Min, Max, Value : Integer; end record; procedure Bump (Arg : in out Int) with Pre'Class => Arg.Value < Arg.Max - 10, Post'Class => Arg.Value > Arg.Value'Old; type Approx_Int is new Int with record Precision : Natural := 0; end record; overriding procedure Bump (Arg : in out Approx_Int) with Post'Class => Arg.Value = Arg.Value'Old + 10; end OO_Example_03;package body OO_Example_03 is procedure Bump (Arg : in out Int) is begin Arg.Value := Arg.Value + 10; end Bump; overriding procedure Bump (Arg : in out Approx_Int) is begin Arg.Value := Arg.Value + 10; end Bump; end OO_Example_03;
This code is correct. Class-wide precondition of Int.Bump
is inherited
by Approx_Int.Bump
. Class-wide postcondition of Approx_Int.Bump
is
stronger than the one of Int.Bump
.
Example #4
package OO_Example_04 is type Int is tagged record Min, Max, Value : Integer; end record; function "+" (Arg1, Arg2 : Int) return Int with Pre'Class => Arg1.Min = Arg2.Min and Arg1.Max = Arg2.Max; type Approx_Int is new Int with record Precision : Natural; end record; -- inherited function “+” end OO_Example_04;
This code is not correct. A type must be declared abstract or "+"
overridden.
Example #5
package OO_Example_05 is type Int is tagged record Min, Max, Value : Integer; end record; procedure Reset (Arg : out Int); type Approx_Int is new Int with record Precision : Natural; end record; -- inherited procedure Reset end OO_Example_05;
This code is not correct. A type must be declared abstract or Reset
overridden Reset
is subject to Extensions_Visible
False
.
Example #6
package OO_Example_06 is type Int is tagged record Min, Max, Value : Integer; end record; procedure Reset (Arg : out Int) with Extensions_Visible; type Approx_Int is new Int with record Precision : Natural; end record; -- inherited procedure Reset end OO_Example_06;package body OO_Example_06 is procedure Reset (Arg : out Int) is begin Arg := Int'(Min => -100, Max => 100, Value => 0); end Reset; end OO_Example_06;
This code is not correct. High: extension of Arg
is not initialized in
Reset
.
Example #7
package OO_Example_07 is pragma Elaborate_Body; type Int is tagged record Min, Max, Value : Integer; end record; function Zero return Int; procedure Reset (Arg : out Int) with Extensions_Visible; type Approx_Int is new Int with record Precision : Natural; end record; overriding function Zero return Approx_Int; -- inherited procedure Reset end OO_Example_07;package body OO_Example_07 is function Zero return Int is ((0, 0, 0)); procedure Reset (Arg : out Int) is begin Int'Class (Arg) := Zero; end Reset; function Zero return Approx_Int is ((0, 0, 0, 0)); end OO_Example_07;
This code is correct. Redispatching ensures that Arg
is fully
initialized on return.
Example #8
package File_System is type File is tagged private; function Closed (F : File) return Boolean; function Is_Open (F : File) return Boolean; procedure Create (F : out File) with Post'Class => F.Closed; procedure Open_Read (F : in out File) with Pre'Class => F.Closed, Post'Class => F.Is_Open; procedure Close (F : in out File) with Pre'Class => F.Is_Open, Post'Class => F.Closed; private type File is tagged record Closed : Boolean := True; Is_Open : Boolean := False; end record; function Closed (F : File) return Boolean is (F.Closed); function Is_Open (F : File) return Boolean is (F.Is_Open); end File_System;package body File_System is procedure Create (F : out File) is begin F.Closed := True; F.Is_Open := False; end Create; procedure Open_Read (F : in out File) is begin F.Is_Open := True; end Open_Read; procedure Close (F : in out File) is begin F.Closed := True; end Close; end File_System;with File_System; use File_System; procedure OO_Example_08 is procedure Use_File_System (F : out File'Class) is begin F.Create; F.Open_Read; F.Close; end Use_File_System; begin null; end OO_Example_08;
This code is correct. State automaton encoded in class-wide contracts is respected.
Example #9
package File_System.Sync is type File is new File_System.File with private; function Is_Synchronized (F : File) return Boolean; procedure Create (F : out File) with Post'Class => F.Closed; procedure Open_Read (F : in out File) with Pre'Class => F.Closed, Post'Class => F.Is_Open and F.Is_Synchronized; procedure Close (F : in out File) with Pre'Class => F.Is_Open and F.Is_Synchronized, Post'Class => F.Closed; private type File is new File_System.File with record In_Synch : Boolean := True; end record; function Is_Synchronized (F : File) return Boolean is (F.In_Synch); end File_System.Sync;package body File_System.Sync is procedure Create (F : out File) is begin File_System.File (F).Create; F.In_Synch := True; end Create; procedure Open_Read (F : in out File) is begin File_System.File (F).Open_Read; F.In_Synch := True; end Open_Read; procedure Close (F : in out File) is begin File_System.File (F).Close; F.Closed := True; end Close; end File_System.Sync;with File_System.Sync; use File_System.Sync; procedure OO_Example_09 is procedure Use_File_System (F : out File'Class) is begin F.Create; F.Open_Read; F.Close; end Use_File_System; begin null; end OO_Example_09;
This code is not correct. Medium: class-wide precondition might be stronger than overridden one
Example #10
package File_System.Sync is type File is new File_System.File with private; function Is_Synchronized (F : File) return Boolean; procedure Create (F : out File) with Post'Class => F.Closed; procedure Open_Read (F : in out File) with Pre'Class => F.Closed, Post'Class => F.Is_Open; procedure Close (F : in out File) with Pre'Class => F.Is_Open, Post'Class => F.Closed; private type File is new File_System.File with record In_Synch : Boolean; end record with Predicate => File_System.File (File).Closed or In_Synch; function Is_Synchronized (F : File) return Boolean is (F.In_Synch); end File_System.Sync;package body File_System.Sync is procedure Create (F : out File) is begin File_System.File (F).Create; F.In_Synch := True; end Create; procedure Open_Read (F : in out File) is begin File_System.File (F).Open_Read; F.In_Synch := True; end Open_Read; procedure Close (F : in out File) is begin File_System.File (F).Close; F.Closed := True; end Close; end File_System.Sync;with File_System.Sync; use File_System.Sync; procedure OO_Example_10 is procedure Use_File_System (F : out File'Class) is begin F.Create; F.Open_Read; F.Close; end Use_File_System; begin null; end OO_Example_10;
This code is correct. Predicate encodes the additional constraint on opened files. Type invariants are not yet supported on tagged types in SPARK.