How to derive own distinguish type from Int?
Asked Answered
O

2

6

I would like to define two data types in Perl 6 deriving from Int but being incompatible with Int or each other at the same time.

For instance:

  • Distance derived from Int with a range 0 up to 32000, and
  • Offset derived from Int with a range from -32000 up to 32000

I would like the types Distance, Offset and Int being distinguishable and incompatible to each other on default.

So (pseudo Perl 6):

  my Distance $d = Distance(12);  // ok
  my Offset $o = Offset(-1);      // ok
  my Distance $d2 = $o;           // BUMMER!

  sub myprint(Int $i) { say $i }

  say $d + $o;                    // BUMMER!
  myprint $d;                     // BUMMER!
  myprint Int($d);                // ok

And so on! I want the Perl 6 compiler to complain if I ever try to mix Distances and Offsets implicitly.

In the books I've read so far there was no hint how to achieve this. Asking Google for some days also offer me no any answer whether this is possible, and if it is, how?

I found about subset but this only put some restriction on a type, but does not render it incompatible to the original type. Furthermore it is not distinguishable from the original type if its restrictions are met in both the original type and its subset.

So I would like to ask here if anybody know if this is possible in Perl 6? And if yes, how would I have to do it?

Openmouthed answered 1/6, 2017 at 9:45 Comment(1)
NB. There's further discussion in SO chat. The most important theme, imo, is the combination of: type checking; compile-time vs run-time; what a dev can do to control when code runs and when type-checking happens; what, in theory, the Rakudo compiler can or can not do; and what it does today. A link to a convenient starting point is chat.stackoverflow.com/transcript/message/37424127#37424127Jiles
G
5

Well if you really want them distinguishable and incompatible by default, just make them totally separate classes. You can define whichever abilities you want. If you use a 'has a' relationship with an integer (instead of an 'is a' relationship), it is very easy to delegate functionality to that value (in this example, I delegate .Int so your example will work):

class Distance
{
    has Int $!value handles<Int>;

    method new($value where 0 <= * <= 32000) { self.bless(:$value) }

    submethod BUILD(:$!value) {}
}

class Offset
{
    has Int $!value handles<Int>;

    method new($value where -32000 <= * <= 32000) { self.bless(:$value) }

    submethod BUILD(:$!value) {}
}

my Distance $d = Distance.new(12); # ok
my Offset $o = Offset.new(-1);     # ok
my Distance $d2 = $o;              # Bummer! Type check fail

sub myprint(Int $i) { say $i }

say $d + $o;                       # Bummer!, can't add those objects 
myprint $d;                        # Bummer!, $d isn't an Int, can't print
myprint Int($d);                   # ok, prints 12, converting with Int

Whatever functionality you want Distance and Offset to have you'll have to build into those classes, possibly delegating to the $!value to make it easy.

EDIT: If you really want your desired syntax my Distance $d = Distance(12); you could add a method to the Int class to call your constructor:

Int.^add_method('Distance', method () { Distance.new(self) });
Int.^compose;

I wouldn't actually advise this -- probably more confusing than helpful. Better to encourage the standard constructor usage. @raiph also points out the idiomatic Perl:

my Distance $d .= new(12);
Grumpy answered 1/6, 2017 at 10:45 Comment(5)
Thank you for this example! I had hoped that there would be a more simpler way like in Ada where I could simply write type Distance is new Integer;. But your answer is a workaround I can chose for the time being. May I ask an addendum? Is there also a possibility to create my e.g. Distance the same like Int i.e. Distance(12) instead of Distance.new(12)?Openmouthed
@user2242237 FYI... I note you have a static-typing tag on your question. The Distance and Offset types are static types. But the compiler complains at compile-time for just the last of your three 'BUMMER' errors. (You can tell this because the message starts with ===SORRY!===.) When you comment that out you get "Type check failed in assignment to $d2;" which is a run-time error. When you comment that out you get "Cannot resolve caller Numeric(Distance: );" which is also a run-time error.Jiles
@Jiles Regarding static type checking: yeah, I had hoped that the compiler would detect all my 'BUMMER' errors. Now that you have found that this happens during runtime, I am a bit disappointed. Can you explain me why the compiler is not able to detect those violations during compile time?Openmouthed
Hi @chi. I only just noticed your comment! (Better late than never?) A Raku compiler is allowed to do as much static analysis as it likes but, 4 years after you wrote this SO, the only Raku compiler that's currently of practical note, Rakudo, currently does relatively little, because analysis makes compilation slower, and a top priority for Rakudo is to quickly start running code. That said, Rakudo is evolving, and after the RakuAST work is completed, it's reasonable to anticipate folk developing deeper static analysis modules.Jiles
"If you really want your desired syntax my Distance $d = Distance(12); you could add a method to the Int class to call your constructor:" ... I wouldn't actually advise this". For anyone reading this, please note that there's been an improvement of Raku's coercion features since this answer was written that might be relevant; importantly, there's no longer a need to modify the Int type in most scenarios -- with the new features you now normally just add coercers to your new types.Jiles
J
3

Update, 2021 And now there's RakuAST![1]

Curt's answer catches the errors chi wanted caught. This complementary answer is my initial exploration of Chi's follow on question asking:

Why, when compiling/running Curt's code, does Rakudo wait until run-time to report two of the three errors?

At the end of this initial exploration I arrive at wrapping Curt's code in a BEGIN block. This code appears to report all errors at compile-time (one by one of course, after commenting out each prior error). (Click for runnable snippet.)[2]

This is a strawman answer, set up for Perl 6 core devs to shoot down.


An initial run of Curt's code results in:

===SORRY!=== ... Calling myprint(Distance) will never work...

The leading ===SORRY!=== signifies a "compile-time"[3] error.

But if we comment out the failing line and try again we get:

Type check failed in assignment to $d2...

This error message is a "run-time"[3] message -- it doesn't start with ===SORRY!===.

Why did the compiler wait until "run-time" to complain?

The answer appears to lie in a combination of Perl 6's mostly-dynamic-by-default nature and the code that fails:

my Distance $d2 = $o;              # Bummer! Type check fail

The my Distance $d2 part of this line is fully processed (introducing a new lexically scoped symbol $d2) when the compiler first encounters this declaration at "compile-time". But the = part of this line is a "run-time" operator; the initializing assignment, and corresponding type check, occur at "run-time".

But a dev may want to force the type check, and hence a type check error, to occur at compile-time. Now what?

BEGIN time

Perl 6 supports program execution space/time travel via phasers. The first phaser is the BEGIN phaser which "Runs at compile time, as soon as possible" and can be used like this:

BEGIN my Distance $d2 = $o;

If you re-compile with the above change, the error now appears at compile-time[3]:

===SORRY!=== Error while compiling...
An exception occurred while evaluating a BEGIN...
Type check failed in assignment to $d2...

If we now comment out the latest failing line and try again we get:

Cannot resolve caller Numeric(Distance: )...

There's no leading ===SORRY!=== so this is again a "run-time" error.

Rerunning the code with the failing line altered to:

BEGIN say $d + $o;

yields:

0
12

on stdout, and on stderr we get:

Use of uninitialized value of type Distance in numeric context...
Use of uninitialized value of type Offset in numeric context...

Uhoh. There's not only no compile-time error, there's no run-time error either! (The run-time warnings may give the game away about the 0. Because the my... lines declaring $d and $o were not prefixed with BEGIN, these symbols have not yet been initialized at compile-time, which is the time that the BEGIN say $d + $o; line gets run. But all of this is moot; we've clearly taken a step backwards.)

What happens if we wrap all of Curt's code in a single BEGIN block?

BEGIN { ... Curt's code goes here ... }

BEGIN {
class Distance...
class Offset...

my Distance $d = Distance.new(12)...

sub myprint(Int $i) { say $i }

say $d + $o;...
}

Bingo! The errors are now all revealed as they were with Curt's original code but the reporting seems to happen at compile-time (one by one, after commenting out each prior error).

Footnotes

[1] In a comment I just wrote below Curt's answer, I've just written[1a]:

Hi @chi. I only just noticed your comment![1a] (Better late than never?) A Raku compiler is allowed to do as much static analysis as it likes but, 4 years after you wrote this SO, the only Raku compiler that's currently of practical note, Rakudo, currently does relatively little, because analysis makes compilation slower, and a top priority for Rakudo is to quickly start running code. That said, Rakudo is evolving, and after the RakuAST work is completed, it's reasonable to anticipate folk developing deeper static analysis modules.

[1a] Of course, I wrote my comment before I looked at my answer! This reflects my process; when I become aware that someone has upvoted (or downvoted) one of my older answers, I typically go check on the question as if I had not answered it -- ignoring my answer -- as a way to quickly get back up to speed on what was asked/said, while I still have a relatively fresh perspective that's not limited by the one I developed when I originally answered it. I had completely forgotten what my answer was, so was naturally struck by the fact it seemed I had not noticed @chi's comment, and had not replied. Further, my immediate reaction was/is that the new RakuAST work was relevant, as jnthn explains, thus my comment, and now my addition of this info to this answer.

[2] Code copied here in case glot.io ever goes away:

# See https://mcmap.net/q/1704766/-how-to-derive-own-distinguish-type-from-int

BEGIN { # to end of file
    
class Distance
{
    has Int $!value handles<Int>;

    method new($value where 0 <= * <= 32000) { self.bless(:$value) }

    submethod BUILD(:$!value) {}
}

class Offset
{
    has Int $!value handles<Int>;

    method new($value where -32000 <= * <= 32000) { self.bless(:$value) }

    submethod BUILD(:$!value) {}
}

my Distance $d = Distance.new(12); # ok
my Offset $o = Offset.new(-1);     # ok
my Distance $d2 = $o;              # Bummer! Type check fail

sub myprint(Int $i) { say $i }

say $d + $o;                       # Bummer!, can't add those objects 
myprint $d;                        # Bummer!, $d isn't an Int, can't print
myprint Int($d);                   # ok, prints 12, converting with Int

}

[3] I scare quoted many references to "compile-time" and "run-time" because they have ambiguous meaning in Perl 6. Perl 6 lets user code do just about anything, including running the compiler, during run-time, and lets user code do just about anything, including run-time things, during compile-time. So from one perspective there can be one or more run-time phases within a compile-time phase and vice versa. But from a second perspective, there's the compile-time phase, i.e. when you're sitting there during a development session and you've just run the compiler. Likewise there's the run time phase, i.e. when your code is, for example, running "in production". Where I do not scare quote run-time / compile-time, I mean to refer to this second perspective.

Jiles answered 5/6, 2017 at 2:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.