I have a lot of functions that can fail, but also have a return type defined in their signature. Since I like defining the types of variables whenever possible, I want to define a Maybe subset to use for this. What I came up with is this:
subset Maybe is export of Mu where Mu | Failure;
The problem with this is Failure
is a subclass of Mu
, so this will match anything and everything, when what I really want is to be able to match one specific type along with Failure
dynamically. My next thought was to create a parameterized role to use as a type, since I don't want to create subsets for every single type that could also be a Failure
. I imagine it looking something like this:
role Maybe[::T] {
# ...
}
sub foo(--> Int) { rand < 0.5 ?? 1 !! fail 'oops' }
my Maybe[Int] $foo = foo;
Only I have no clue what I'd need to add to the role in order to make this work. Is it possible to create a role like this? If not, is there another way I could create a type to do what I want?
Perl6 types are already Maybe types.
It's just that Perl6 has typed nulls unlike most other languages with Maybe types.
This is Maybe[Int]
variable:
my Int $a;
my Int:_ $a; # more explicit
This holds a definite Int
:
my Int:D $a = …; # must be assigned because the default is not “definite”
This holds a null Int
:
my Int:U $a;
Note that Failure
is a subtype of Nil
, so even subroutines that have a return type specified can return them.
(Nil
is not like null
or nil
from other languages.)
sub foo ( --> Int:D ) { Bool.pick ?? 1 !! fail 'oops' }
my $foo = foo; # $foo contains the failure object
Nil
is really a type of generic soft failure. When assigned to a variable it just resets it to the default.
my Int $foo = 1;
$foo = Nil;
say $foo.perl; # Int
my Int:D $bar is default(42) = 1;
$bar = Nil
say $bar.perl; # 42
The typical default is the same as the type.
my Int $foo;
say $foo.VAR.default.perl; # Int
A specific soft failure would be to return a type object
sub foo ( --> Int ){
Bool.pick ?? 1 !! Int
}
That is why I said Nil
is a “generic” soft failure.
Generally if you are defining the type of a variable, you want it to be of that type. So your code should complain immediately if it gets something of another type.
There are better ways to deal with a Failure
.
sub foo(--> Int:D ) { rand < 0.5 ?? 1 !! fail 'oops' }
with foo() -> Int:D $foo {
… # use $foo here
} else -> $fail {
… # use $fail here
}
This works because Failure
always sees itself as being undefined.
You can also use that with when
given foo() {
when Int:D -> Int:D $foo {
… # use $foo here
}
when Failure:D -> Failure:D $fail {
# a DEFINITE Failure value
# (`DEFINITE` is different than `defined`.)
}
default {
… # handle unexpected values (can be removed if not needed)
}
}
Or just the defined-or operator //
if you don't care what kind of failure it is.
my Int:D $foo = foo() // 1;
You may even want to use that to turn a Failure
into a Nil
.
my Int:D $foo is default(42) = foo() // Nil;
If you really want a maybe-failure subset, I think this should work:
sub Maybe-Failure ( Any:U ::Type ) {
anon subset :: of Any where Type | Failure
}
my constant Maybe-Int = Maybe-Failure(Int);
# note that the type has to be known at compile-time for this line:
my Maybe-Int $foo = foo;
It doesn't currently work though.
(Note that you should not be dealing with Mu
unless you need to specifically deal with the types and values that are outside of the Any
type; like Junction
and IterationEnd
.)
Something else that should probably also work is:
my class Maybe-Failure {
method ^parameterize ( $, Any:U ::Type ) {
anon subset :: of Any where Type | Failure
}
}
my Maybe-Failure[Int] $foo;
This seems like it fails for the same reason the other one does.
Another way would be to create a new type of class like subset
.
That is subset
uses a different MOP than the rest of the classes in Perl6.
TL;DR See @Kaiepi's own answer for a solution. But every non-native type in P6 is already automatically an enhanced nullable type that's akin to an enhanced Maybe type. So that needs to be discussed too. To help structure my answer I'm going to pretend it's an XY problem even though it isn't.
I want to define a
Maybe subset
to use for this
See @Kaiepi's answer.
The subset
solution is overkill for what wikipedia defines as a Maybe type which boils down to:
None
[or] the original data type
It turns out that all non-native P6 types are already something akin to an enhanced Maybe type.
The enhancement is that the (P6 equivalent of a) None
knows what original data type it's been paired with:
my Int $foo;
say $foo # (Int) -- akin to an (Int) None
I have a lot of functions that can fail, but also have a return type defined in their signature.
As you presumably know, unless use fatal;
is in effect, P6 deliberately allows routines to return failures even if there's a return type check that doesn't explicitly allow them. (A subset
return type check can explicitly reject them.)
So given that a return type check Foo
is automatically turned into something akin to a subset
with a where Failure | Foo
clause, it's understandable that you thought to accommodate that by creating an matching subset so you could accept the result when assigning to a variable.
But as is hopefully clear from the earlier discussion, it may be better to make use of the built in aspect of P6's type system that's akin to Maybe types.
A Nil
may be used to indicate what's called a benign failure. So the following works to indicate failure (as you wish to do in some of your routines) and set a receiving variable to a None
(or rather the enhanced P6 equivalent of one):
sub foo (--> Int) { Nil }
my Int $bar = foo;
say $bar; # (Int)
So one option is that you replace calls to fail
with return Nil
(or just Nil
).
One could imagine a pragma (called, say, failsoft
) that demotes all Failure
s to benign failure Nil
s:
use failsoft;
sub foo (--> Int) { fail }
my Int $bar = foo;
say $bar; # (Int)
The wikipedia introduction about Maybe types also says:
A distinct, but related concept ... is called nullable types (often expressed as
A?
).
The closest P6 equivalent to the Int?
syntax used by some languages to express a nullable Int
is simply Int
, without the question mark. The following are valid type constraints:
Int
-- P6 equivalent of a nullable Int
or a Maybe Int
Int:D
-- P6 equivalent of a non-nullable Int
or a Just Int
Int:U
-- P6 equivalent of an Int
null or an (Int) None
(:D
and :U
are called type smilies for an obvious reason. :))
Continuing, wikipedia's Nullable types page says:
In statically-typed languages, a nullable type is [a Maybe] type (in functional programming terms), while in dynamically-typed languages (where values have types, but variables do not), equivalent behavior is provided by having a single null value.
In P6:
Values have types -- but so do variables.
P6 types are akin to an enhanced Maybe
type (as explained above) or an enhanced nullable type where there are as many None
s or "null" values as there are types instead of having just a single None
or null value.
(So, is P6 a statically typed language or a dynamically typed language? It's actually Beyond static vs dynamic and is instead static and dynamic.)
Continuing:
Primitive types such as integers and booleans cannot generally be null, but the corresponding nullable types (nullable integer and nullable boolean, respectively) can also assume the
NULL
value.
In P6, all non-native types (like the arbitrary precision Int
type) are akin to enhanced Maybe/nullable types.
In contrast, all native types (like int
-- all lowercase) are non-nullable types -- what wikipedia is calling primitive types. They cannot be null or None
:
my int $foo;
say $foo; # 0
$foo = int; # Cannot unbox a type object (int) to int
Finally, returning to the wikipedia Maybe
page:
The core difference between [maybe] types and nullable types is that [maybe] types support nesting
(Maybe (Maybe A) ≠ Maybe A)
, while nullable types do not(A?? = A?)
.
P6's built in types don't support nesting in this way without use of subsets. So a P6 type, while akin to an enhanced Maybe
type, is really just an enhanced nullable type.
Brad Gilbert's answer pointed me in the right direction, particularly:
Another way would be to create a new type of class like subset. That is subset uses a different MOP than the rest of the classes in Perl6.
The solution I came up with is this:
use nqp;
class Maybe {
method ^parameterize(Mu:U \M, Mu:U \T) {
Metamodel::SubsetHOW.new_type:
:name("Maybe[{T.^name}]"),
:refinee(nqp::if(nqp::istype(T, Junction), Mu, Any)),
:refinement(T | Failure)
}
}
my Maybe[Int] $foo = 1;
say $foo; # OUTPUT: 1
my Maybe[Int] $bar = Failure.new: 2;
say $bar.exception.message; # OUTPUT: 2
my Maybe[Int] $baz = 'qux'; # Throws type error
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With