Domain-Specific Languages in Ada SPARK

Method Chaining

The advantage of method chaining in Ada is that it can be implemented using only tagged records. No access types or other means of indirection are needed. It passes SPARK’s gnatprove even without annotations. No Ada2022 is necessary.

The disadvantage of method chaining is that it depends on tagged types which might be unavailable, e.g., in systems lacking a secondary stack.

with Computers; use Computers;

procedure Main
  with
    SPARK_Mode => On
is
begin

   Computer
     .Processor
       .Cores (2)
       .Speed (2500)
       .I386
     .Disk
       .Size (150)
     .Disk
       .Size (75)
       .Speed (7200)
       .Sata
     .Done;

end Main;
package Computers
  with
    SPARK_Mode => On
is

   type Computer_Context is tagged private;

   function Null_Computer_Context
     return Computer_Context;

   type Processor_Context is tagged private;

   function Null_Processor_Context
     return Processor_Context;

   type Disk_Context is tagged private;

   function Null_Disk_Context
     return Disk_Context;

   ------------------------------------------------------------

   procedure Done
     (CC : Computer_Context);

   procedure Done
     (PC : Processor_Context);

   procedure Done
     (DC : Disk_Context);

   function Processor
     (CC : Computer_Context'Class)
     return Processor_Context;

   function Processor
     (PC : Processor_Context'Class)
     return Processor_Context;

   function Processor
     (DC : Disk_Context'Class)
     return Processor_Context;

   function Disk
     (CC : Computer_Context'Class)
     return Disk_Context;

   function Disk
     (PC : Processor_Context'Class)
     return Disk_Context;

   function Disk
     (DC : Disk_Context'Class)
     return Disk_Context;

   ------------------------------------------------------------

   function Cores
     (PC : Processor_Context;
      N  : Positive)
     return Processor_Context;

   function Speed
     (PC : Processor_Context;
      N  : Natural)
     return Processor_Context;

   function Speed
     (DC : Disk_Context;
      N  : Natural)
     return Disk_Context;

   function Size
     (DC : Disk_Context;
      N  : Natural)
     return Disk_Context;

   function I386
     (PC : Processor_Context)
     return Processor_Context;

   function Sata
     (DC : Disk_Context)
     return Disk_Context;

   function Computer
     return Computer_Context;

private

   type Processor_Context is tagged
   null record;

   type Disk_Context is tagged
   null record;

   type Computer_Context is tagged
   null record;

end Computers;

Procedure Sequence and Named Parameters

Procedure sequence and named parameters result in DSLs that are very close to Ada’s native programming model. The composability is established from the sequence and Ada is sequential anyway. In addition, named parameters help to cut back on internal context management.

with Computers; use Computers;

procedure Main
  with
    SPARK_Mode => On
is
begin

   Computer;
     Processor (Cores => 2,
                Speed => 2500,
                Arch  => I386);
     Disk (Size  => 150);
     Disk (Size  => 75,
           Speed => 7200,
           Bus   => Sata);
   Done;

end Main;

Assignment and Overloaded Operators

Overloaded operators allow me to mimick enumeration by leveraging unary and binary overloaded operators. The GNAT Spitbol Patterns library is a nice showcase how a DSL can be constructed from overloaded operators.

They work well as right-hand-sides of variable assignments since operators can appear only in expressions. The disadvantage of handling variables is that we have to use them.

with Computers; use Computers;

procedure Mchain
  with
    SPARK_Mode => On
is
   C : Computer :=
     - Processor
         * Cores (2)
         * Speed (2500)
         * I386
     - Disk
         * Size (150)
     - Disk
         * Size (75)
         * Speed (7200)
         * Sata;
begin
   Done (C);
end Mchain;

Communication

Advancing DSL-like interfaces from the role of a developer can quickly result in adverse situations. Peers quickly notice the focus on representation. It stands out from the conventional state-manipulation style.

Arguing that an interface needs to promote extensibility falls short: The client pays for the extension, not the extensibility.

Arguing that an interface needs to clearly reflect the domain model falls short: Preference follows habit not clarity.

The situation is aggravated by the fallacy that if AI was unaffected by all that limits human cognition and if software did not need to be read and written by humans anymore, then we could let slip good software design.