From: Adam Beneschan on 16 Nov 2009 12:43 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 16 Nov 2009 15:28 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 16 Nov 2009 15:32 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 16 Nov 2009 16:35 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 16 Nov 2009 17:28
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 |