Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a Maybe type in Perl 6

Tags:

types

raku

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?

like image 974
Kaiepi Avatar asked Mar 08 '19 23:03

Kaiepi


3 Answers

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.

like image 174
Brad Gilbert Avatar answered Oct 21 '22 17:10

Brad Gilbert


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.

Solving Y

I want to define a Maybe subset to use for this

See @Kaiepi's answer.

All non-native P6 types are already akin to Maybe types

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

Solving X

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 Failures to benign failure Nils:

use failsoft;
sub foo (--> Int) { fail }
my Int $bar = foo;
say $bar; # (Int)

Nullable types

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 Nones 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.

like image 8
raiph Avatar answered Oct 21 '22 17:10

raiph


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
like image 7
Kaiepi Avatar answered Oct 21 '22 17:10

Kaiepi