Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What's the point of proto anyway? #440

Open
ab5tract opened this issue Sep 24, 2024 · 6 comments
Open

What's the point of proto anyway? #440

ab5tract opened this issue Sep 24, 2024 · 6 comments
Labels
language Changes to the Raku Programming Language

Comments

@ab5tract
Copy link

ab5tract commented Sep 24, 2024

While discussing multi dispatch in #raku-dev recently, I came to realize that I have no idea how these proto things are actually meant to be used.

Beyond their clear utility in being a place to "hang" their candidates, my understanding was that they could be used to define the "shape" of these candidates by defining details about their signatures.

To this end, I expected all of the following to be true:

  1. The return type of a proto constrains the the return type of multi candidates to that return type or its descendants.

  2. The parameter types of a proto similarly constrain the parameter types of its candidates

  3. The number of parameters in a proto constrains the number of parameters in its candidates.

  4. The container choice of parameters in a proto constrain the container choices of its candidates.

Of the above, I'm pleased to report that points 2 and 3 are, in fact, true.

But why would 1 and 4 be false?

Return types: Working as documentated

Apparently, return types are not constrained by design. No mention is made of container choice.

The following example is given, citing some benefit that is apparently too obvious to be worth clarification:

Since proto is a wrapper for multi candidates, the signatures of the routine's multi candidates do not necessarily have to match that of the proto; arguments of multi candidates may have subtypes of those of the proto, and the return types of the multi candidates may be entirely different from that of the proto. Using differing types like this is especially useful when giving proto a function body: (emphasis mine)

enum DebugType <LOG WARNING ERROR>;
 
#|[ Prints a message to stderr with a color-coded key. ] 
proto debug(DebugType:D $type, Str:D $message --> Bool:_) {
    note sprintf qb/\e[1;%dm[%s]\e[0m %s/, {*}, $type.key, $message
}
multi debug(LOG;; Str:D --> 32)     { }
multi debug(WARNING;; Str:D --> 33) { }
multi debug(ERROR;; Str:D --> 31)   { }

(Note: Let's save the "why" of the use of double-semicolons for some other time)

I definitely don't mean this as a slight at our documentation efforts. If I were documenting this aspect of proto, I would by necessity be equally vague in any efforts to explain this part of the dispatch design. That is to say, I cannot explain what I cannot fathom.

What is the point of defining an interface only to then arbitrarily violate that interface?

I understand that TMTOWTDI is sacrosanct. In most cases, I agree with it. But we provide the ability for enums to have arbitrarily complex values. Maybe it's tilting at windmills to push back against this specific case by saying "just encode the ASCII color number as a value of the DebugType".

Maybe there's a very strong case for this behavior. I'm often easy to convince in the weight of good evidence.

Or maybe this is just a generally confusing case of YAGNI.

At some point we need to ask ourselves where we want to draw the line in terms of language complexity and readability.

Is it really so much to ask of a user to write a stand-alone wrapper sub that calls a sub of a different name in the rare, intractable cases that can't be solved via all the avenues we've already paved?

Containers

Since $ can always hold a @ or a %, the following is mostly unsurprising:

proto m($) {*}
multi m(%h) { dd :%h }
m %(hello => "there");
#=> :h({:hello("there")})

But I don't understand why this would not be encoded as a compile-time error:

proto m(@) {*}
multi m(%h) { dd :%h }
m %(hello => "there");
#=> :h({:hello("there")})

Instead of a SORRY, it actually succeeds instead.

And it gets worse:

proto m(Int @) {*}
multi m(%h) { dd :%h }
m %(hello => "there");
#=> :h({:hello("there")})

What is the point of a proto anyway?

Outro

I'd much prefer for a proto to be a way to encode a compile-time interface for its multi candidates.

It's very easy to say "accept everything", in fact that's why many proto look like proto foo(|) {*}. So why be so loosey-goosey when it comes to scenarios where the proto is specifically narrowed in its declaration?

The wrapper functionality feels totally "against user intentions" to me if it requires allowing mutable return types and ignores clear distinctions I'm attempting to make about the shape of candidate parameters.

@ab5tract ab5tract added the language Changes to the Raku Programming Language label Sep 24, 2024
@lizmat
Copy link
Collaborator

lizmat commented Sep 24, 2024

FWIW, proto foo(|) {*} is only required in the core setting. In user Raku code it is only needed when you want to tag it with is export.

@ab5tract
Copy link
Author

FWIW, proto foo(|) {*} is only required in the core setting. In user Raku code it is only needed when you want to tag it with is export.

I'm not complaining about proto foo(|) {*}. I only mentioned it because there is a clear way to declare that you want to allow your candidates to "take any shape". (And if you don't declare a specific proto, this is exactly the proto "shape" we install synthetically for you).

@timo
Copy link

timo commented Sep 24, 2024

"proto sub" and "proto method" (along with "only sub" and "only method") are also necessary when you want to prevent multi candidates from an "outer" scope to enter into the candidate pool. for subs that is literal outer scopes, for methods that is superclasses.

in other words, if you derive from a class that has a few multis that you don't want to be accessible, you can declare a new proto.

@patrickbkr
Copy link
Member

I seem to recall that protos had a long and painful way to come to be. I think some early 6guts posts touch on that topic. Maybe I'll manage to dig them up.

@patrickbkr
Copy link
Member

That's the one:

https://6guts.wordpress.com/2010/10/17/wrestling-with-dispatch/

@librasteve
Copy link

librasteve commented Oct 2, 2024

seems that protos are trying to fulfill (at least) two goals:
(i) make a common wrapper for the multis
(ii) make a common high level type check for the multis

in the case of the example you cite (which I had to run myself to "grok"):

enum DebugType <LOG WARNING ERROR>;
 
#|[ Prints a message to stderr with a color-coded key. ] 
proto debug(DebugType:D $type, Str:D $message --> Bool:_) {
    note sprintf qb/\e[1;%dm[%s]\e[0m %s/, {*}, $type.key, $message
}
multi debug(LOG;; Str:D --> 32)     { }
multi debug(WARNING;; Str:D --> 33) { }
multi debug(ERROR;; Str:D --> 31)   { }

There is a bunch of formatting in the proto. Putting it there is a convenience that avoids needing it in each multi. This implements case (i). It wouldn't work if the Bool:_ return type constraint is applied to each multi. I guess the chosen solution to the dilemma was to limit the proto return constraint to what the proto returns. I guess it was felt that this proto functionality was helpful for DRY across the multis and avoided the need to go for an explicit wrapper.

So - I read this part of the docs as a "QED" that protos cannot provide (i) unless their return constraint is limited to the return value of the proto and not the multis. LTA docs for sure, but otherwise logical (though you may not agree with this design decision).

I share your concerns about container type constraints and suggest that these being missing is a bug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
language Changes to the Raku Programming Language
Projects
None yet
Development

No branches or pull requests

5 participants