11

I am trying to define a subroutine in Raku whose argument is, say, an Array of Ints (imposing that as a constraint, i.e. rejecting arguments that are not Arrays of Ints).

Question: What is the "best" (most idiomatic, or straightforward, or whatever you think 'best' should mean here) way to achieve that?

Examples run in the Raku REPL follow.

What I was hoping would work

> sub f(Int @a) {1}
&f
> f([1,2,3])
Type check failed in binding to parameter '@a'; expected Positional[Int] but got Array ([1, 2, 3])
  in sub f at <unknown file> line 1
  in block <unit> at <unknown file> line 1

Another non-working example

> sub f(@a where *.all ~~ Int) {1}
&f
> f([1,2,3])
Constraint type check failed in binding to parameter '@a'; expected anonymous constraint to be met but got Array ([1, 2, 3])
  in sub f at <unknown file> line 1
  in block <unit> at <unknown file> line 1

even though

> [1,2,3].all ~~ Int
True

What does work

Both variations

> sub f(@a where { @a.all ~~ Int }) {1}

and

> sub f(@a where { $_.all ~~ Int }) {1}

do give me what I want:

> f([5])
1
> f(['x'])
Constraint type check failed in binding to parameter '@a'; expected anonymous constraint to be met but got Array (["x"])
  in sub f at <unknown file> line 1
  in block <unit> at <unknown file> line 1

I've used that in the past, but it strikes me as a bit clumsy/verbose..

Additional remark

The syntax Int @a I initially tried isn't completely bogus, but I can't tell when it's supposed to go through and when not.

I can do this in a class for instance:

> class A { has Int @.a }
(A)

> A.new(a => [1,2,3])
A.new(a => Array[Int].new(1, 2, 3))

> A.new(a => [1,2,'x'])
Type check failed in assignment to @!a; expected Int but got Str ("x")
  in block <unit> at <unknown file> line 1

Edit 1

As per the docs, this works

> sub f(Int @a) {1}
&f

> my Int @a = 1,2,3 # or (1,2,3), or [1,2,3]
> f(@a)
1

But if I omit the Int in the declaration of @a I get back to the errors reported before. And as noted, I can't run f on anonymous arrays, with f([1,2,3]).

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
grobber
  • 1,083
  • 1
  • 9
  • 20

2 Answers2

10

I think the main misunderstanding is that my Int @a = 1,2,3 and [1,2,3] are somehow equivalent. They are not. The first case defines an array that will only take Int values. The second case defines an array that will take anything, and just happens to have Int values in it.

I'll try to cover all versions you tried, why they didn't work, and possibly how it would work. I'll be using a bare dd as proof that the body of the function was reached.

#1

sub f(Int @a) { dd }
f([1,2,3])

This doesn't work because the signature accepts a Positional that has an Int constraint on its container descriptor. Signature binding only looks at the constraints of arguments, not at the values. Observe:

my Int @a; say @a.of;   # (Int)
say [1,2,3].of;         # (Mu)
say Mu ~~ Int;          # False

There is no solution to this approach, as there is no [ ] syntax that generates an Array with an Int constraint.

#2

sub f(@a where *.all ~~ Int) { dd }

This is very close, but the use of * is not doing the right thing. I'm not sure whether this is a bug or not.

You've found these solutions also work:

sub f(@a where { @a.all ~~ Int }) { dd }
sub f(@a where { $_.all ~~ Int }) { dd }

Fortunately, you do not have to actually specify the explicit block. This also works:

sub f(@a where @a.all ~~ Int) { dd }
sub f(@a where $_.all ~~ Int) { dd }

Apart from the @a.all and $_.all solutions you found, there is a third solution: just throw out the *!

sub f(@a where .all ~~ Int) { dd }

#3

class A { has Int @.a }
A.new(a => [1,2,3])

This is different from signature binding. Effectively in the .new you're doing a:

@!a = [1,2,3]

And that works because there are only Int values in the array that you specified. As you showed, of there's anything else in there, it will fail. But that is no different from:

my Int @a = [1,2,"foo"]

failing.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
  • 1
    Thank you for that thorough breakdown! I believe this sets me straight. And yes, you've identified the root cause of the misunderstanding in the `[1,]` vs `my Int @a = [1,]` issue. As for the [WhateverCode](https://docs.raku.org/type/WhateverCode) version using `*.all ~~ Int`, I too don't know whether it's a bug. I find `Whatever` stars have surprising behavior in a number of settings, which is why I use mostly blocks in [map](https://docs.raku.org/routine/map) / [grep](https://docs.raku.org/routine/grep) etc. But that is perhaps a different question. – grobber Jun 15 '21 at 11:38
  • Can the `where` clause be re-written using the `.ACCEPTS` method instead of smart-matching? What would that look like? Thx! – jubilatious1 Jun 15 '21 at 23:10
  • 1
    `A ~~ B` is just syntactic sugar for `B.ACCEPTS(A)`, so you're already using `.ACCEPTS` under the hood? But to answer your question: you can't really. Even writing it as `where { .all ~~ Int }.ACCEPTS($_)` doesn't make much sense, becomes the outer `where` would effectively doing a `Bool.ACCEPTS($_)` and that always returns the `Bool`. – Elizabeth Mattijsen Jun 16 '21 at 09:28
4

This is intended as a supplement to Liz's already-accepted and correct answer.

First, note that there's one additional difference between sub f(Int @a) {...} and sub f(@a where .all ~~ Int) {...}: the first checks the type of the Array (an O(1) operation) while the second iterates through the Array and checks the type of each element (an O(n) operation). This came up in a previous question, which you might find helpful.

Second, there's another way to write f that takes advantage of the new coercion protocol (and is probably how I'd write it, personally):

sub f(Array[Int]() $a) {...}

This restricts $a to any type that can be converted to an Array[Int] and then binds it to that typed Array. This is broadly similar to @a where .all ~~ Int, except that it uses a $ and maintains the type constraint inside the function.

codesections
  • 8,900
  • 16
  • 50
  • 4
    Thanks! The answer adds valuable info, as does the link to the earlier question. Incidentally, let me note that that earlier question could arguably (dis)qualify mine as a duplicate, but people seem to be less trigger-happy about that sort of thing in the `Raku` corner of `SO`. Thanks for that too! :) – grobber Jun 15 '21 at 21:07