Raku / problem-solving

🦋 Problem Solving, a repo for handling problems that require review, deliberation and possibly debate
Artistic License 2.0
70 stars 16 forks source link

No way of having new versions of CORE classes for new language revisions #71

Closed vrurg closed 5 years ago

vrurg commented 5 years ago

Turns out there is no way to have another implementation of the same class in another CORE.setting. It could have the same name, would successfully perform under it's corresponding language pragma. But... Those with HEAD installed can try the following:

type-match.pm6:

use v6.c;
unit module type-match;
sub caller is export { CALLER:: }

type-match.p6:

#use v6.e.PREVIEW;
use v6.c;
use type-match;
say MY::.WHAT ~~ caller.WHAT;

This code outputs True with use v6.c in type-match.p6 but False with use v6.e.PREVIEW! And it makes sense because another implementation of PseudoStash is defined CORE.e.setting.

What implications does it has? While trying to solve #47 I realized that just re-declaring QuantHash and adding Iterable role to it is not enough, roles and classes consuming it must also migrate into CORE.e.setting. Next, I stumble upon a method where an arbitrary object must be coerced into Set. The Set method for this is defined in Any. It is creating a new Set object. But Any resides in CORE.setting and the method will create object using 6.c version of Set!

Do I need to move Any too? But that would mean moving just plain everything (as everything depends on Any)! And this is where problems are starting. What are we gonna do about all routines with signatures referring to Any? Because we now have two of those. Wether the signature would match or not will depend on which language version the object has been constructed under!

For now I have only the description of the problem, not a single idea of how to deal with it.

vrurg commented 5 years ago

To extend the subject a little bit, I'd like to confirm what was expected: two user modules of two different versions provide two different classes too. What it could end up with? Say, a class Foo provided by two versions of Foo modules, namely v0.1.0 and v0.2.0.

Then, lets assume there're 3rd party classes Bar1 and Bar2 we use. The former one is older and inherits from Foo:ver<0.1.0> whereas the latter is newer and inherits from Foo:ver<0.2.0>. Then here is what we get:

say Bar1.^mro;
say Bar2.^mro;
say Bar1.^mro[1] ~~ Bar2.^mro[1];
say Bar1.^mro[1].^ver;
say Bar2.^mro[1].^ver;

outputs:

((Bar1) (Foo) (Any) (Mu))
((Bar2) (Foo) (Any) (Mu))
False
v0.1
v0.2.0

Because the end-user may not and must not know the internals of the modules he is using this situation would be absolute LTA to him.

vrurg commented 5 years ago

Just some tentative thoughts.

The core of the problem is types not matching to each other. I think there must be a way to proclaim a type object to be a different version of an existing one. Few options I have in my mind:

Second problem is coercion methods on Any as well as any other use of a type object. But it would require separate resolution in each use case.

jnthn commented 5 years ago

Turns out there is no way to have another implementation of the same class in another CORE.setting.

Well, if it's a different implementation, is it in any sense the same class?

Let's take the specific example of QuantHash, and the proposal that it becomes Iterable in 6.e. The Iterable role is used to control flattening behavior. If we were to type-constrain a variable to the Set of 6.c, and depend on it functioning as an item in flattening context, then it would be quite appropriate that a 6.e Set would be considered incompatible - because it could break the behavior of the code. That's just type constraints doing their job.

(As an aside: I'm not sure whether it even should become Iterable - my recollection is that Set is not by design, and that was intentional in order to ease sets of sets; alas, searching the IRC log doesn't seem to work so I can't easily dig up the years-ago discussion, but @TimToady may recall more on that).

So far as modules go, I think the common case is that we can evolve roles and classes without breaking their compatibility. A :ver on the type would thus be a mechanism to indicate incompatibility - at which point, once again, we can argue that types are doing their job in rejecting an incompatible value being used that will behave against the expectations of the code consuming it.

vrurg commented 5 years ago

The whole thing stems from rakudo/rakudo#1203. And to my view when

my %s1 is Set = <a b c>; my %s2 is Set = %s1; say %s2

results in:

set(set(a b c))

It goes totally against DWIM and common logic. If a set of sets is needed then braces would serve well for this, for example.

Especially contr-intuitive are Bags and Mixes which are even closer to Hash semantics.

I played with iterable QuantHash a bit and things looked to be way more natural than it's current implementation. To conclude, I would say that the experiment of making it non-iterable failed.

Now, back to the roots. I actually agree that a new class version is a new class by itself. The more I think of it, the less reasonable I see the idea of proclaiming different versions as the same entity.

But lets assume that it is decided that QuantHash must be an iterable after all and we get new versions of Set and its family. It's ok for pre-6.e code to ignore this fact and be able to deal with the older versions only. But 6.e code does know about the older classes. How would they handle this situation? Say, a method doesn't care about the new semantics and same code would work well for both versions. Should it be something like:

use v6.e;
...
subset VSet of Any where Set:ver<6.c> | Set:ver<6.e>;
method append (VSet $elems) {
    $!collection ∪= $elems;
}
method contains(VSet $elems) {
    $elems.Set ⊂ $!collection
}

The second method would coerce any Set version into Set:ver<6.e>. VSet could be replaced with Set:ver<*>, for example.

Another problem remains: coercion methods on other classes. Again, as a typical example: what Set must use Any.Set()? The current implementation will always use 6.c version because compile-time resolution is used. For correct behavior run-time resolution must be used. One way would be:

method Set {
    CLIENT::CORE::<Set>.new(self);
}

Or with special quoting: Q::«Set», or with a new pseudo-package CC::<Set> which is just an alias for CLIENT::CORE::.

Perhaps these two changes could work to solve the problem.

lizmat commented 5 years ago

As far as I can remember, when I asked TimToady what the behaviour of that situation should be, he decided for the current behaviour. AKA, QuantHashes are an item.

I can see the inconsistency. I have no problem in fixing that inconsistency.

vrurg commented 5 years ago

@lizmat hope I get you right: the current behavior remains unchanged? If so, I would close all related tickets, including rakudo/rakudo#1203 and #47 .

lizmat commented 5 years ago

No, that's not what I'm saying. I'm open to change, but I will not take that decision.

vrurg commented 5 years ago

Ok, noted. ;)

Whichever way it goes, the problem of introducing new implementations of classes would stick around anyway. So, I'd like to know if it makes sense in trying to implement typeobject versioning via :ver<>. Since ClassHOW and ParametricRoleHOW do Versioning but don't set version for core objects, it is possible to set it based on what core is being compiled. This would allow user-land code to correct its behaviour if necessary by checking for, say

Set.^ver eq '6.e'

or by multidispatching:

multi method foo(Set:ver<6.d>) { ... }
multi method foo(Set:ver<6.e>) { ... }
vendethiel commented 5 years ago

I know that 1) It’s most probably very very difficult to implement and has nothing quite like it yet 2) it doesn’t help much the Set situation 3) it doesn’t help resolve several classes with different :auth<> 4) has classes thinking they’re the same 5) ...probably some other things. But I’d still like to mention Ruby’s Refinements. Their design might be of interest, if only to say « no, we do not want that »

Here’s an example, pseudo code with Ruby syntax.

module Perl6e; # should this be a simple package?
refine Any {
  method Set { ... }
}
refine Set {
  method append { ... }
}

This is Ruby’s way of making monkey patching lexical (instead of what’s usually done in Ruby...), but it looks a tiny bit similar to our issue at hand

vrurg commented 5 years ago

Closing via rakudo/rakudo#3112