<skip>-able Arguments

A <skip> parameter is one where instead of getting a type error on a mismatch, it will set the parameter to null:

 foo: function [a :b [<skip> tag!] c] [
     dump [a b c]
 ]

 >> foo 1 2
 a: 1
 b: ; null
 c: 2

 >> foo 1 <hello> 2
 a: 1
 b: <hello>
 c: 2

You can only skip a "hard quoted" parameter. To understand why, imagine a having an evaluative skippable parameter followed by a quoted argument. If you evaluate the first argument--see it doesn't match the type--you can't really rewind time to then switch into a quoting mode for the second argument on the thing you already evaluated. It would be incoherent.

Beyond the technical reason you don't want <skip> on evaluated parameters, it also makes it clearer to the user. For instance, consider tagged compose:

>> compose <*> [(1 + 2) (<*> 1 + 2) (1 + 2)]
== [(1 + 2) 3 (1 + 2)]

You can tell at the source level that's tagged. But what if it said:

>> compose foo [(1 + 2) (<*> 1 + 2) (1 + 2)]
== ???

Does this mean that sometimes the block is taken by the compose and sometimes it's not? What if FOO is a block? The "shape" of the code changes, and that seems bad.

So it's for quoted arguments only...and that's probably for the best.

It's used by DEFAULT

The use in tagged compose is pretty obvious. But a trickier usage is DEFAULT, which quotes a SET-WORD! or SET-PATH! on its left to assign to it:

 >> x: null

 >> x: default [print "running" 1 + 2]
 running
 == 3  ; didn't have a value, so ran the body and assigned

 >> x: default [print "not run!" 10 + 20]
 == 3  ; already had a value, don't run the block and leave alone

The trick skippability brings to the table is that if there is no SET-WORD! or SET-PATH!, then it just runs the block. This makes it a nice tool in evaluative switch/case:

 >> case [
     1 > 2 [print "nope" 10]
     default [print "yep!" 20]
 ]
 yep
 == 20

CASE and SWITCH are written to just let the last value fall out of them.

 >> switch "not a number" [
      1 + 2 [...]
      3 + 4 [...]
      5 + 6
 ]
 == 11

So all DEFAULT does is fall back to acting like DO when it can't find a SET-WORD! or SET-PATH! to its left.

 >> switch "not a number" [
      1 + 2 [...]
      3 + 4 [...]
      default [5 + 6]
 ]
 == 11

It comes off as quite readable. (Though if you are lazy, or code golfing, you can just omit it...maybe some people will like that better.)

The implementation is now solid, so this feature is sticking around

The initial implementation was a bit creaky, especially when it came to skipping left hand side arguments. I was worried it might not be able to work efficiently--or maybe it wasn't able to work in a general way at all!

But things have firmed up very well. Impressively enough, this works:

>> foo: function [/a :/b [<skip> integer!]] [
    reduce [(a else [<null>]) (b else [<null>])]
]

>> foob: enfix :foo/b

>> parameters of :foob
== [:b /a]

>> 10 foob
== [<null> 10]

>> "not an integer!" foob
== [<null> <null>]

>> 10 -> foob/a 20
== [20 10]  ; (note: broken at last check, looking into it)

And we can discuss what ("not an integer!" -> foob/a 20) should do, if that should be an error or not.

Anyway, FYI, that's a pretty wild case. If it can do that, it can probably work for whatever you want it to do. So start thinking of creative uses of <skip> in your own code...and if you do, post it in the thread here!

1 Like

Skippable parameters have become an important piece of the system, making things like the tight notation for predicates possible:

>> any [2 4 6 7 10]
== 2

>> any .not.even? [2 4 6 7 10]  ; detects blank-headed TUPLE! at callsite
== 7

It was quickly apparent that skippable parameters would have to be hard quoted. That's the only way they make sense at a callsite.

But if you supply the predicate via a refinement, you generally don't want it to be hard-quoted:

>> any/predicate [2 4 5 7 10] func [x] [x > 6]
== 7

That should be the same parameter, but invoking it with a path should change it.

Kill Two Birds With One Stone: Quoted Refinement Means <skip> ?

I'm not sure how often it comes up that you need a quoted refinement argument, in the general case. I've never needed it.

Parameter quoting for convenient callsite interfaces of common functionality. That's sort of at odds with what refinements do... they're not the common case, or if they were, they'd be normal arguments.

It could make some sense to put two and two together to say '/arg being both "quoted at callsite" and "optional" means it has a position in the callsite list. And then if you use it in a path invocation, the quote does not apply.

So when I talk about packing the actual parameter spec (minus name) into a single value, '/[tuple!] could then mean skippable tuple, and you wouldn't need to say '[<skip> tuple!].

It sacrifices true quoted refinement arguments...where you invoke an optional parameter through a path and the argument is quoted. It's one of those "a little less powerful" things that someone, somewhere, would complain about.

>> do-a-thing [...]

>> do-a-thing/label [...] foo
** Error: foo not defined
      ^-- "hey, I wanted label to be quoted!"

But I think my argument sort of holds, in that if you were willing to type out / l a b e l then what's one more apostrophe to you?

>> do-a-thing/label [...] 'foo

Callsite quoting is a peculiarity to be used in special cases to get expressive power comparable to other languages. Once you've gone and made a refinement argument, it seems that's not the case anymore.

I've certainly become attached to the feature of skippable parameters. Of course, anything that is going to be "meta" about making function calls has to decide what to do with them.

One example would be APPLY. Consider that COMPOSE has a first parameter which is the "Label" that can be (for instance) a tag to label the compose slots.

If you don't provide a label, all the GROUP!s are composed.

>> compose [(1 + 2) (<*> 3 + 4)]
== [3 7]

But if you do provide a label, only the GROUP!s starting with that label get composed:

>> compose <*> [(1 + 2) (<*> 3 + 4)]
== [(1 + 2) 7]

So when APPLY is processing parameters in order, what should it do about the label? It's evaluating the arguments in the block, so I don't think that's very compatible with the hard-quote rule of skippability.

Hence I propose that APPLY always skips over the skippable parameters positionally, and they must be provided explicitly by name.

>> apply :compose [[(1 + 2) (<*> 3 + 4)]]
== [3 7]  ; assumed first parameter was the block, not the label

>> apply :compose [[(1 + 2) (<*> 3 + 4)] /label <*>]
== [(1 + 2) 7]  ; you can still provide a label, just by name

>> apply :compose [[(1 + 2) (<*> 3 + 4)] /label second [<x> <*>]]
== [(1 + 2) 7]  ; evaluations are okay, since you explicitly named it

This seems the most sensible behavior.

1 Like