comp.lang.ada
 help / color / mirror / Atom feed
From: Hadrien Grasland <hadrien.grasland@gmail.com>
Subject: Re: Musings on RxAda
Date: Wed, 21 Oct 2015 04:45:36 -0700 (PDT)
Date: 2015-10-21T04:45:36-07:00	[thread overview]
Message-ID: <d1fb3d04-ec7a-4563-9fd0-4a8e3c33b5aa@googlegroups.com> (raw)
In-Reply-To: <mvlopk$49j$1@dont-email.me>

Interesting problem ! Let me try to play with it too.

If you need asynchronicity, the standard Ada answer is tasks. So here, you would like an "observable task", which can hold data of any type, perform any action on this data, and return data of another (possibly different) type.

The type-safe and statically checkable way to hold data of any type is to build a generic package. So I would argue that this is the idiomatic Ada way to solve this problem, and a better alternative to the type erasure you attempt to implement using null interfaces. It also has the important advantage of working with built-in Ada tyes.

If try to make a generic task which fits our needs, we might get something like this as a first attempt (NOTE : This code is wrong, read below to know why) :

====================

generic
   type Input_Data is private;
   type Output_Data is private;
package Observables is
   
   -- Any valid mapping function for this observable can be matched by this access type
   type Mapping_Function is access function (Input : Input_Data) return Output_Data;
   
   -- Any subscribing procedure can be matched by this access type
   type Subscribing_Proc is access procedure (Input : Input_Data);
   
   -- This task performs the asynchronous mapping
   task type Observable is
      entry Just (Data : Input_Data; Output : access Observable);
      entry Just (Data : Input_Data; Action : Subscribing_Proc);
      entry Map (Mapping : Mapping_Function; Output : access Observable);
      entry Map (Mapping : Mapping_Function; Action : Subscribing_Proc);
   end Observable;
   
end Observables;

====================

Notice that Map and Just must return results through a parameter, because Ada task entries cannot be functions. This is somewhat annoying, but probably motivated by the fact that a return in an accept statement would be highly confusing.

Notice also that I have removed Subscribe, and replaced it by overloadings of Just and Map. That is because an Ada task may return results immediately within the bodies of Just and Map, in which case subscribing later would only be a recipe for overhead and race conditions.

More problematic, however, are the facts that...

   1/ This code is illegal. Task entries Map and Just cannot take an access parameter.
   2/ Even if it were legal, it wouldn't do what we want. The Observer that would be passed as a parameter to Map would expect Input_Type as input, whereas we would want it to accept Output_Type as input and do not care about its output.

The "do not care" statement above is a telltale sign that we need another layer of abstraction. We need something that can hold data (results) of a given type, is thread-safe, and allows us not to think about the data after it is sent.

I would argue that this something is a protected object :

====================

generic
   type Data_Type is private;
package Async_Data is
   
   -- This container is promised to hold a chunk of data someday
   protected type Datum is
      procedure Send (Result : Data_Type);
      entry Receive (Result : out Data_Type);
   private
      Data_Ready : Boolean := False;
      Data : Data_Type;
   end Datum;
   
end Async_Data;

====================

This is really a textbook example of a protected type, and indeed you will find one like this in almost all Ada textbook. Nevertheless, for the sake of completeness, here is an implementation :

====================

package body Async_Data is
   
   protected body Datum is
      
      procedure Send (Result : Data_Type) is
      begin
         Data := Result;
         Data_Ready := True;
      end Send;
      
      entry Receive (Result : out Data_Type) when Data_Ready is
      begin
         Result := Data;
      end Receive;
      
   end Datum;
   
end Async_Data;

====================

Okay, so now we have asynchronous communication channels. We can use them to rewrite the Observable code above so that its Map function actually sends data to an asynchronous output channel, and that it can accept asynchronous input as well, like so :

====================

with Async_Data;

generic
   with package Async_Input is new Async_Data (others => <>);
   with package Async_Output is new Async_Data (others => <>);
package Observables is
   
   -- Let's clarify our input and output data types
   subtype Input_Data is Async_Input.Data_Type;
   subtype Output_Data is Async_Output.Data_Type;
   
   -- Any valid mapping function for this observable can be matched by this access type
   type Mapping_Function is access function (Input : Input_Data) return Output_Data;
   
   -- Any subscribing procedure can be matched by this access type
   type Subscribing_Proc is access procedure (Data : Output_Data);
   
   -- This task performs the asynchronous mapping
   task type Observable (Input : access Async_Input.Datum) is
      entry Map (Mapping : Mapping_Function; Output : in out Async_Output.Datum);
      entry Map (Mapping : Mapping_Function; Action : Subscribing_Proc);
   end Observable;
   
end Observables;

====================

Notice that I have suppressed the "Just" entry, whose complexity has become unnecessary due to our use of asynchronous input objects. Also, here is a possible implementation :

====================

package body Observables is

   task body Observable is
      Data : Input_Data;
   begin
      Input.Receive (Data);
      select
         accept Map (Mapping : Mapping_Function; Action : Subscribing_Proc) do
            Action.all (Mapping (Data));
         end Map;
      or
         accept Map (Mapping : Mapping_Function; Output : in out Async_Output.Datum) do
            Output.Send (Mapping (Data));
         end Map;
      end select;
   end Observable;
   
end Observables;

====================

How would we use such an object ? Well, any self-respecting would have a bunch of predefined instances of these generic packages for standard types, like this one :

====================

with Async_Data;
with Observables;

package Predefined is
   
   -- In a real library, basic asynchronous data types like this would be predefined
   package Async_Characters is new Async_Data (Character);
   subtype Async_Character is Async_Characters.Datum;
   package Async_Naturals is new Async_Data (Natural);
   subtype Async_Natural is Async_Naturals.Datum;
   
   -- Same for basic observables like this
   package Char_To_Nat_Observables is new Observables (Async_Input  => Async_Characters,
                                                       Async_Output => Async_Naturals);
   subtype Char_To_Nat_Observable is Char_To_Nat_Observables.Observable;
   package Nat_To_Char_Observables is new Observables (Async_Input  => Async_Naturals,
                                                       Async_Output => Async_Characters);
   subtype Nat_To_Char_Observable is Nat_To_Char_Observables.Observable;
   
end Predefined;

====================

And we would then want to use them like this :

====================

with Ada.Text_IO;
with Predefined;

procedure Main is
   -- Here, we will try to map a character to a natural and back, then print the result
   function Char_To_Nat_Mapping (Char : Character) return Natural is (Character'Pos (Char));
   function Nat_To_Char_Mapping (Nat : Natural) return Character is (Character'Val (Nat));
   
   procedure Print_Char (Char : Character) is
   begin
      Ada.Text_IO.Put (Char);
   end Print_Char;
   
   -- Let's try it !
   Input : constant Character := 'a';
   Char_To_Nat : Predefined.Char_To_Nat_Observable (new Predefined.Async_Character);
   Nat_To_Char : Predefined.Nat_To_Char_Observable (new Predefined.Async_Natural);
begin
   Char_To_Nat.Input.Send (Input);
   Char_To_Nat.Map (Char_To_Nat_Mapping'Access, Nat_To_Char.Input.all);
   Nat_To_Char.Map (Nat_To_Char_Mapping'Access, Print_Char'Access);
end Main;

====================

Sadly, this does not work. The Ada accessibility rules prevent us to use the access to subprograms Char_To_Nat_Mapping, Nat_To_Char_Mapping, and Print_Char, on the grounds that the task might subsequently leak pointers to nonexistent subprograms. So we would need to declare our functions in a separate package.

And this is where I will stop, because I think I have made my point : yes, it is possible to implement something like the Observers you mentioned in Ada. But I think it is highly unlikely to be anywhere near as practical as it is in C# or Java.

The reason lies in Ada's strict enforcement of pointer accessibility rules, including for function pointers, something which garbage-collected languages need not care about but which is of critical importance for languages where scope determines variable and function lifetimes.

Have a nice day !
Hadrien

  parent reply	other threads:[~2015-10-21 11:45 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2015-10-14 14:30 Musings on RxAda Alejandro R.  Mosteo
2015-10-15 14:40 ` brbarkstrom
2015-10-21 11:45 ` Hadrien Grasland [this message]
2015-10-21 12:12 ` Hadrien Grasland
2015-10-21 13:35   ` Dmitry A. Kazakov
2015-10-21 16:18     ` Hadrien Grasland
2015-10-21 16:47       ` Dmitry A. Kazakov
2015-10-21 19:09         ` Hadrien Grasland
2015-10-21 19:35           ` Dmitry A. Kazakov
2015-10-21 21:04             ` Hadrien Grasland
2015-10-22 11:02               ` Alejandro R.  Mosteo
2015-10-22 12:33                 ` Dmitry A. Kazakov
2015-10-22 16:41                   ` Alejandro R.  Mosteo
2015-11-19 13:14 ` Jacob Sparre Andersen
replies disabled

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox