You are making unwarranted assumptions here. I am writing about
language which is probably unknown to you. In this language
interface inheritance is mostly static (some folks would say
purely static). Interfaces are useful for polymorphism, but
in _this_ case polymorphism is parametric and would be useful
even without any dynamic aspect. You could have similar (but
more clumsy) language with interfaces without inheritance:
interface inheritace allows nice expression of common parts and
by name test for interface (in)compatibility (otherwise one
would be forced to use structural tests). One could use
interface inheritance without polymorphism. For example in
language somewhat similar to Modula2 or UCSD Pascal one could
have modules with separate interfaces and use inheritance for
common parts (but have no OO nor overloading).
Interfaces are related to overloading: visible interfaces decide
which signatures are visible. And in language that I am writing
above overloading means choice of _signature_. Actual function
to be run is determined at runtime.
>
> > - when overloading allows more than one function choice is
> > "arbitrary"
>
> Arbitrary? That sound like a very poorly designed language.
>
> > Note: in case of exact match local signature wins.
>
> = #1
>
> > Overloading
> > only plays role when there are multiple alternatives leading to
> > different types.
>
> = #2
>
> > Both rules work under assumption that design has right names,
> > so functons with the same signatur do equivalent work.
>
> Well, this is the Liskov's substitutability principle (LSP). Whether to
> apply it to ad-hoc polymorphism is a question of program semantics.
>
> In any case, the language does not interfere with the semantics, that
> would be incomputable.
Well, theorem proving is uncomputable, but proof checking is computable.
Arguably, program should be deemed correct only when programmer
can justify its correctness. So, at least in theory you could have
language that requires programmer to include justification (that is
proof) of correctnes...
> LSP is not not enforceable, it is only a design
> principle.
>
> > Expirience with 200 thousend lines codebase shows that
> > this works well. However, it is not clear how this would work
> > with bigger codebase (say 20 million lines) or in less regular
> > problem domain.
>
> Large projects are split into weakly coupled modules.
This 200 thousend lines is split into about 1000 modules. But
there is cluster of about 600 mutually connected modules. From
one point of view many of those modules are "independent", but
there are subtle connections and if you transitively track them there
is largish cluster...
> > Arguably, signaling error when overloading can not be resolved
> > in unique way would be safer.
>
> That is the only way. The question is different. Whether visibility
> rules should allow programs with *potentially* unresolvable overloading.
> E.g. both M1 and M2 declare conflicting Foo. M3 uses both M1 and M2 but
> does not reference Foo. Is M3 legal? Or just the fact of using
> conflicting M1 and M2 should make M3 illegal.
Word "potentially" have many different meanings. In my case
types are hairy enough that there are 3 cases:
- compiler can decide that there is only one applicable signature
- compiler can decide that there is conflict
- compiler can not decide and only at runtime it possible to
decide if there is a conflict.
The core of problem may be illustrated using Extended Pascal.
Namely, one can define schema type:
type T(i : integer) = 0..100;
above type discriminant 'i' is otherwise unused but ensures
that later w get incompatible types.
We can have procedure taking argument of schema type:
function foo(a : T) : integer;
....
But we can also use discriminanted version of type:
function foo(a : T(42)) : integer;
....
We can have another function taking integer parameters:
function bar(i : integer) : integer;
var a : T(i) = 0;
begin
bar := foo(a);
end;
Extended Pascal does not have overloading, so you can not have both
foo-s in the same context. But suppose that we add overloading
keeping other rules. Then, if i is different than 42 only
general (schematic) version is applicable, so there is unique
applicable siganture. But when i = 42, then both calls would
be valid, so there is conflict. As a little variation one
could write:
function bar(i : integer) : integer;
var a : T(i*i) = 0;
begin
bar := foo(a);
end;
Since 42 is not a square the second case can not occur, so there
would be no conflict at runtime.
Of course, in Ada spirit it would be to disallow both versions.
OTOH even in Ada some checks are delayed to runtime...
> >> The first point is that from the SW development POV, if the imported
> >> module's interface changes, the client code must be reviewed anyway.
> >
> > Well, if your coding rules say the review is in place, do what rules
> > say. However, adding new signature without changing behaviour of
> > existing signatures may be "safe" change. Namely, it is safe
> > for languages without overloading.
>
> It is never safe. The change can always introduce a conflict. The only
> choice is between flagging it as an error always or only when a client
> actually uses it.
Well, assuming that there are no conflict between two different
imported signatures, but only imported signature gets shadowed by
local one you will get the same code. Of course, after adding
new export compilation may discover that added signature is
in conflict with other imported signature, but this is different
case.
> >> The second point is that the rule #1 does not actually protect clients.
> >> As an example consider two modules used by the client. If one of the
> >> modules introduces a name conflict with another module, that breaks the
> >> client anyway, because this case cannot be handled by the rule #1 anymore.
> >
> > Sure. But there is important difference: global object must be
> > coordinated to avoid conflicts.
>
> There should be no global objects, for the start.
Sorry, you are attaching different meaning to words that I am.
To have meaningful disscussion we need common (global to the
disscussion) understanding of "global" and "object". To put
it differently common words should be global objects. Similarly
to be able to program you need global objects. For example
in C++ everything at source level is in some namespace. Eliminate
global namespace and you can no longer program in C++ (you would
probably consider this a good thing, but equvalent thing applies
to Ada). In particular some interfaces must be global.
> > Extended Pascal significanly decreases chance of
> > artificial conflict, that is accidentally importing name that
> > client do not want to use.
>
> You cannot accidentally import anything. The module interface must be
> designed rationally to be useful for the clients. There is no defense
> against poorly designed interfaces.
Well, there is no law of nature saying that poorly designed interfaces
are impossible. Actually, appearence of poorly designed interfaces
seem to be natural state. So, unless you can control 100% of
codebase that you use you are likely to be forced to use
some poorly designed interface. Of couse, you may hide such
interface behind a better designed one, but this was exactly
my point: you need some way to insulate your code from unwanted
external interfaces.
> However interfaces can be designed in favor of clients deploying fully
> qualified names vs clients deploying direct visibility. Unfortunately
> one should choose one.
>
> The former tends to choose same names for everything. So that the client
> does something like:
>
> IO.Integers.Put;
>
> The latter would rather do
>
> IO.Integer_IO.Put_Integer
>
> which with direct visibility becomes
>
> Put_Integer
Well, with overlaoding it is normal to use direct visibility and
use the same name for all types: overloading take care of types
and you rarely need qualified names. Of course, you need to
properly choose names so that they correspond well with actual
meaning, but this is easier if you do not have to invent new
names just to avoid conflicts...
> > OTOH explicit import/export lists
> > are less attractive when there is overloading (to get equivalent
> > effect they require repeating type information).
>
> Why? Explicit import in Ada is very popular. I gave example of
>
> use type T;
>
> clause which is that thing.
No, this is not explicit import/export list. I meant something
like:
import signature foo : U -> V from T
meaning that you want from T only function foo having argument of
type U and return value of type V. In principle T may export
20 different foo-s, each with its own signature, so saying
import foo from T
would be ambigious (reasonably it could import all foo-s, but
that may be too much). And of course problem is most acute
when there is established "canonical" name like '+' or 'map'
(96 overloads in my codebase).
This is different than what you showed: you use named interface.
> Usually such types are numeric which leads
> to massive overloading of +,-,*,/. Nobody cares as these are all resolvable.
In my case probably "most overloaded" is 'coerce' (238 overloads).
It is "changing type" and wrong version would not pass typechecking.
> > Concerning Ada way, it is not clear for me how this is supposed
> > to work when there are multiple intersecting subsets of functions.
>
> I am not sure what you mean.
You showed how to import whole interface (all exported signatures).
But how you handle situation when you have module exporting several
signatures and in different uses you need different subsets.
Say, for files one client wants 'open', 'read', 'close'. Another
Works with existing files and wants 'read' and 'write'. Package
for file operations is likely to contains several other operations.
I did not specify types above, but at least for some operations
it is natural to provide overloaded variants. In principle
client may want arbitrary subset of exported operations. Clearly
creating separate interface per subset is not feasible.
In principle one could have one interface per operation, but
this looks clumsy. Can Ada do better?
> Overloading works pretty well Ada because
> of types. Languages shy of overloading are ones with weak type system
> when everything gets lumped into something unresolvable. E.g. if there
> is no distinct numeric types or when the result is not a part of the
> signature etc.
Well, I would say "explicit types". ML and Haskell have have
rather detailed types. But they use type inference and
algorithm they use depends on "almost" lack of overloading
(they have cludges to overload arithmetic).
--
Waldek Hebisch