From: Adam Beneschan on
On Nov 13, 12:12 pm, xorque <xorquew...(a)googlemail.com> wrote:
> Hello.
>
> I'm trying to write an archive/directory abstraction in a similar vein
> to PhysicsFS
> but am at a bit of a loss as to how to design the archiver interface:
>
> with Path;
>
> package Archiver is
>
>   type Archiver_t              is abstract tagged limited private;
>   type Archiver_Class_Access_t is access Archiver_t'Class;
>
>   procedure Init
>     (Archiver : out Archiver_t) is abstract;
>
>   function Can_Mount
>     (Archiver : in Archiver_t;
>      Path     : in Path.Real_t) return Boolean is abstract;
>
>   type File_t              is abstract tagged limited private;
>   type File_Class_Access_t is access File_t'Class;
>
>   procedure Open
>     (Archiver : in     Archiver_t;
>      Path     : in     Path.Virtual_t;
>      File     :    out File_t) is abstract;
>
>   procedure Close
>     (File : in out File_t) is abstract;
>
> private
>
>   type Archiver_t is abstract tagged limited null record;
>
>   type File_t is abstract tagged limited null record;
>
> end Archiver;
>
> The idea of the above is that the main part of the library only deals
> with
> archivers and "files" (which might only really be pointers to entries
> in Zip
> files, for example) by 'Class.
>
> The problem with the above is that:
>
> archiver.ads:18:13: operation can be dispatching in only one type
>
> Hopefully someone here knows a better way to handle this.

I don't know what PhysicsFS is, so I'm not clear on what you are
trying to accomplish.

Maybe your plan is to define concrete types derived from Archive_T and
File_T that go together, in pairs. For example, a programmer might
define one package Archive_1 that defines Archive_1_T and File_1_T,
and another user might write a package Archive_2 that defines
Archive_2_T and File_2_T, but the intent is that the two would be
dependent---i.e. you would always use Open with two objects that go
together, such as an object of type Archive_1_T with a File_1_T, or an
Archive_2_T with a File_2_T, but never with an Archive_1_T and a
File_2_T. In that case, you would make one parameter classwide, as
Dmitry suggested:

procedure Open
(Archiver : Archiver_t'Class; -- Class-wide
Path : Virtual_t;
File : in out File_t) is abstract;

Then an implementation would need to check manually to make sure the
type is correct:

procedure Open
(Archive : Archiver_T'Class;
Path : Virtual_T;
File : in out File_1_T) is
begin
if Archive not in Archive_1_T then
raise <some exception> with "Open on File_1_T used with wrong
archiver class";
end if;
declare
The_Arch : Archive_1_T renames Archive_1_T(Archive);
begin
...
... and now you have an Archive_1_T and a File_1_T to work with.

(I haven't checked this to make sure it's correct. But it would be
something like this.)

By the way, for a while I was considering requesting a language change
to allow this sort of restricted multiple dispatch, in cases where a
primitive operation of two types could be declared and inherited for
two derived types that are derived in the same package, and when
dispatching, the program would check to make sure the two actual types
really were declared in the same package. This would avoid having to
deal with M x N combinations of types---something that I think is an
issue with MI---since it only deals with pairs (or triplets, etc.) of
types that are defined to "go together". I could try to write a
proposal if there's enough interest, but I think it's too late to get
into the next language revision.

If your plan is that Archiver and File types be more independent,
though, what you may need to do is to pick one of the parameters to
Open, say Archive (as above) to be a class-wide type, and then provide
enough operations on Archiver_T so that an implementation of Open
could dispatch on Archive to do whatever needs to be done with the
archive.

If you have some other idea about the relationship between Archiver
and File, then you might need to give me more specifics about how you
intend your package to be used.

-- Adam
From: Dmitry A. Kazakov on
On Mon, 16 Nov 2009 09:43:06 -0800 (PST), Adam Beneschan wrote:

> By the way, for a while I was considering requesting a language change
> to allow this sort of restricted multiple dispatch, in cases where a
> primitive operation of two types could be declared and inherited for
> two derived types that are derived in the same package, and when
> dispatching, the program would check to make sure the two actual types
> really were declared in the same package. This would avoid having to
> deal with M x N combinations of types---something that I think is an
> issue with MI---since it only deals with pairs (or triplets, etc.) of
> types that are defined to "go together".

But you still have these combinations independently on where you derive.
You allow the "diagonal" of the full dispatching table. I.e. the
combinations:

A1, B1,
A2, B2,
A3, B3

But other combinations are still there, because they can be spelt. It is
just so that they raise Constraint_Error. This "diagonal" behavior is what
we already have for multi-methods (a form of MD with only one hierarchy
A=B). I suppose this is one of the motivations of your proposal.

But the problem with "diagonal" dispatch is that it is inconsistent with
the idea of static typing. I would insist on the design rule that dispatch
shall never fail in a legal program. I.e. the compiler shall enforce all
possible combinations of types no later than at compile time. Further, it
shall not "invent" broken implementations. In "diagonal" dispatch it would
do (and does) exactly this, it would override A1, B2 with an implementation
raising Constraint_Error.

At the same time the case represented by "diagonal" dispatch is very
important in other situations, like parallel hierarchies of types. E.g.
when we wanted to force a new instance from B'Class for each new instance
from A'Class. But again that enforcement shall be static. BTW, it is not
multiple dispatch. Technically this is rather single dispatch where the
tuple of types (A, B) is considered as root of some class of tuples, in
which (A1, B1), (A2, B2) are instances. Presently we have nothing in the
language to handle this (except the jack-of-all-trades, generics). It would
be interesting to speculate how tuples can be supported in Ada, especially
their flattening in the arguments lists, and using tuples as multiple
results of a function.

> I could try to write a
> proposal if there's enough interest, but I think it's too late to get
> into the next language revision.

I am incredibly interested in MD (and in tuples as well), but I think we
should not touch it until we knew how to do it right.

--
Regards,
Dmitry A. Kazakov
http://www.dmitry-kazakov.de
From: Dmitry A. Kazakov on
On Mon, 16 Nov 2009 21:28:00 +0100, Dmitry A. Kazakov wrote:

> At the same time the case represented by "diagonal" dispatch is very
> important in other situations, like parallel hierarchies of types. E.g.
> when we wanted to force a new instance from B'Class for each new instance
> from A'Class. But again that enforcement shall be static. BTW, it is not
> multiple dispatch. Technically this is rather single dispatch where the
> tuple of types (A, B) is considered as root of some class of tuples, in
> which (A1, B1), (A2, B2) are instances. Presently we have nothing in the
> language to handle this (except the jack-of-all-trades, generics). It would
> be interesting to speculate how tuples can be supported in Ada, especially
> their flattening in the arguments lists, and using tuples as multiple
> results of a function.

I forgot to say, that in this case illegal combinations like A1, B2 are
statically illegal, because there would be no tuple (A1, B2) declared.

--
Regards,
Dmitry A. Kazakov
http://www.dmitry-kazakov.de
From: Adam Beneschan on
On Nov 16, 12:28 pm, "Dmitry A. Kazakov" <mail...(a)dmitry-kazakov.de>
wrote:
> On Mon, 16 Nov 2009 09:43:06 -0800 (PST), Adam Beneschan wrote:
> > By the way, for a while I was considering requesting a language change
> > to allow this sort of restricted multiple dispatch, in cases where a
> > primitive operation of two types could be declared and inherited for
> > two derived types that are derived in the same package, and when
> > dispatching, the program would check to make sure the two actual types
> > really were declared in the same package.  This would avoid having to
> > deal with M x N combinations of types---something that I think is an
> > issue with MI---since it only deals with pairs (or triplets, etc.) of
> > types that are defined to "go together".
>
> But you still have these combinations independently on where you derive.
> You allow the "diagonal" of the full dispatching table. I.e. the
> combinations:
>
> A1, B1,
> A2, B2,
> A3, B3
>
> But other combinations are still there, because they can be spelt. It is
> just so that they raise Constraint_Error. This "diagonal" behavior is what
> we already have for multi-methods (a form of MD with only one hierarchy
> A=B). I suppose this is one of the motivations of your proposal.
>
> But the problem with "diagonal" dispatch is that it is inconsistent with
> the idea of static typing. I would insist on the design rule that dispatch
> shall never fail in a legal program. I.e. the compiler shall enforce all
> possible combinations of types no later than at compile time.

Since a call to a dispatching operation may have parameters that are
themselves of class-wide types, I don't think this is possible.

In Ada as it exists currently, you can declare a primitive operation
with two parameters of the same tagged type (maybe that's what you
meant by "multi-method"---sorry, I don't know the jargon). E.g.

type T is abstract tagged null record;
procedure Some_Operation (Param_1 : T; Param_2 : T);

If you later call

Some_Operation (X, Y);

and X is declared to be of type T1 (a descendant of T) and Y is
declared to be of type T2 (also a descendant of T), then the compiler
can statically determine that this will fail. But if either X or Y
(or both) is declared to be of type T'Class, then the compiler can't
statically tell whether the dispatch will fail or not. So a runtime
check is needed. Similarly for the proposal I was thinking of making.


> Further, it
> shall not "invent" broken implementations. In "diagonal" dispatch it would
> do (and does) exactly this, it would override A1, B2 with an implementation
> raising Constraint_Error.
>
> At the same time the case represented by "diagonal" dispatch is very
> important in other situations, like parallel hierarchies of types. E.g.
> when we wanted to force a new instance from B'Class for each new instance
> from A'Class. But again that enforcement shall be static. BTW, it is not
> multiple dispatch. Technically this is rather single dispatch where the
> tuple of types (A, B) is considered as root of some class of tuples, in
> which (A1, B1), (A2, B2) are instances. Presently we have nothing in the
> language to handle this (except the jack-of-all-trades, generics). It would
> be interesting to speculate how tuples can be supported in Ada, especially
> their flattening in the arguments lists, and using tuples as multiple
> results of a function.
>
> > I could try to write a
> > proposal if there's enough interest, but I think it's too late to get
> > into the next language revision.
>
> I am incredibly interested in MD (and in tuples as well), but I think we
> should not touch it until we knew how to do it right.

I don't think the proposal I was considering was a solution to MD, but
rather to a smallish subset of MD-related problems---although it was a
subset I thought would be useful, and I may have run into a case in my
own code where I would have wanted to use it.

-- Adam
From: Dmitry A. Kazakov on
On Mon, 16 Nov 2009 13:35:00 -0800 (PST), Adam Beneschan wrote:

> On Nov 16, 12:28�pm, "Dmitry A. Kazakov" <mail...(a)dmitry-kazakov.de>
> wrote:
>> On Mon, 16 Nov 2009 09:43:06 -0800 (PST), Adam Beneschan wrote:
>>> By the way, for a while I was considering requesting a language change
>>> to allow this sort of restricted multiple dispatch, in cases where a
>>> primitive operation of two types could be declared and inherited for
>>> two derived types that are derived in the same package, and when
>>> dispatching, the program would check to make sure the two actual types
>>> really were declared in the same package. �This would avoid having to
>>> deal with M x N combinations of types---something that I think is an
>>> issue with MI---since it only deals with pairs (or triplets, etc.) of
>>> types that are defined to "go together".
>>
>> But you still have these combinations independently on where you derive.
>> You allow the "diagonal" of the full dispatching table. I.e. the
>> combinations:
>>
>> A1, B1,
>> A2, B2,
>> A3, B3
>>
>> But other combinations are still there, because they can be spelt. It is
>> just so that they raise Constraint_Error. This "diagonal" behavior is what
>> we already have for multi-methods (a form of MD with only one hierarchy
>> A=B). I suppose this is one of the motivations of your proposal.
>>
>> But the problem with "diagonal" dispatch is that it is inconsistent with
>> the idea of static typing. I would insist on the design rule that dispatch
>> shall never fail in a legal program. I.e. the compiler shall enforce all
>> possible combinations of types no later than at compile time.
>
> Since a call to a dispatching operation may have parameters that are
> themselves of class-wide types, I don't think this is possible.

No, what I have in mind is that each time a new type is derived, we could
somehow ensure that the whole row M+1 (from N x M+1) is filled either by a
overriding or per a reasonable inheritance. The major problem is that we
cannot see all Ns when we do M+1.

The idea of deriving both types in one package looks like an attempt to
control the places where we can expand the table N x M. That is not enough
to enforce completeness of the table in the above sense.

There should be something more strong, but also more liberal in terms of
modularization. Obviously to derive from all types related via a shared
primitive operation in one package is awful (and sometimes just impossible
within the given parent-child hierarchy of packages).

> In Ada as it exists currently, you can declare a primitive operation
> with two parameters of the same tagged type (maybe that's what you
> meant by "multi-method"---sorry, I don't know the jargon). E.g.
>
> type T is abstract tagged null record;
> procedure Some_Operation (Param_1 : T; Param_2 : T);
>
> If you later call
>
> Some_Operation (X, Y);
>
> and X is declared to be of type T1 (a descendant of T) and Y is
> declared to be of type T2 (also a descendant of T), then the compiler
> can statically determine that this will fail. But if either X or Y
> (or both) is declared to be of type T'Class, then the compiler can't
> statically tell whether the dispatch will fail or not. So a runtime
> check is needed. Similarly for the proposal I was thinking of making.

Yep, this is what I meant. The compiler should require to override *all*
combinations:

type T2 is new T1 with ...;
overriding
procedure Some_Operation (X : T2; Y : T2);
-- Error!

overriding
procedure Some_Operation (X : T2; Y : T2);
overriding
procedure Some_Operation (X : T1; Y : T2);
overriding
procedure Some_Operation (X : T2; Y : T1);
-- OK

Yes, this is tedious, but the programmer should know what we does when he
declares a multi-method. This is the semantics of - there are n**2
combinations which do potentially different things. If there are only n
combinations, that is another case, which should *not* look MD, because it
is not. I think it should somehow use tuples instead. The error should
happen upon tuple construction, not in the call. For the reader it should
be clear that there is only one tag. I have no idea how to spell that
syntactically or by other means.

Al for "true" multi-methods I think we should support delegation in order
to control combinatorial explosion. For example:

1. declaration Some_Operation commutative;

2. explicit delegation of T2 x T1 to T1 x T2

overriding
procedure Some_Operation (X : T2; Y : T1)
renames Some_Operation with (X => Y, Y => X);

3. delegation of T1 x T2 to T2 x T2 supplying a downcast conversion from T1
to T2

overriding
procedure Some_Operation (X : T1; Y : T2)
renames Some_Operation with (X => (X with ...), Y => Y);

4. delegation of T2 x T2 to T1 x T2 (upcast)

overriding
procedure Some_Operation (X : T2; Y : T2)
renames Some_Operation with (X => T1 (X), Y => Y);

etc.

--
Regards,
Dmitry A. Kazakov
http://www.dmitry-kazakov.de