LAMBDA and YIELDER and Specifying "Return" Specs

Actions that lack RETURN functions might nevertheless want to document their potential return product.

intsum: lambda [x [integer!] y [integer!]] [
    x + y  ; want to document this will be an integer
]

twoints: yielder [n [integer!]] [
    yield n + 10, yield n + 20  ; want to document it yields integers
]

The yielder seems pretty obviously like you should say yield: [integer!]:

twoints: yielder [yield: [integer!] n [integer!]] [
    yield n + 10, yield n + 20
]

It's a rather direct parallel to FUNC: YIELD is the word you're using (like RETURN) and it's definitional to the FRAME! so you can't have another parameter called YIELD. You could have one called RETURN, though:

twoints: yielder [yield: [integer!] return [integer!]] [
    yield return + 10, yield return + 20
]

But LAMBDA is a different beast. Especially if you're building a FUNC-like construct on top of LAMBDA, you're likely going to define a frame local called RETURN.

We could just say that you put return: [integer!] in the spec anyway, and understand that there's no actual return function.

intsum: lambda [return: [integer!] x [integer!] y [integer!]] [
    x + y  ; want to document this will be an integer
]

But I feel like it's good to reinforce that there isn't a return, and there's no word in the frame.

There could be another annotation or part of speech, like #result

intsum: lambda [
    #result [integer!]
    x [integer!]
    y [integer!]
][
    x + y
]

Or we could think about dropping a label entirely, and saying you just understand the first block is a description of the result:

intsum: lambda [
    [integer!]
    x [integer!]
    y [integer!]
][
    x + y
]

But the speedbump on that is that it flies in the face of my preferred syntax, which is to put the parameter label before the type spec. This provides a good rhythm that you don't need to break when you have refinements with no arguments.

You'd have to put it after, or it would be considered part of the function description.

intsum: lambda [
    "An Integer Sum Expressed as a Lambda"
    [integer!] "Result is the sum of first and second"
    x "The first value"
        [integer!]
    y "The second value"
        [integer!]
][
    x + y
]

Mechanically, New Ideas Are Coming

It may be that the way to typecheck a lambda in the event you want that is to use another component:

intsum: returns [integer!] lambda [
    "An Integer Sum Expressed as a Lambda"
    x "The first value"
        [integer!]
    y "The second value"
        [integer!]
][
    x + y
]

Maybe it could notice if you put a block-in-a-block for a description:

intsum: returns [
    "Description of return value"
        [integer!]
] lambda [
    "An Integer Sum Expressed as a Lambda"
    x "The first value"
        [integer!]
    y "The second value"
        [integer!]
][
    x + y
]

And it's just that FUNCTION and YIELDER make it a bit easier by rolling that all together for you. Which is an interesting thought, though it's kind of ugly.

Outside The Box Thinking: Colons With No Words?

A single colon, perhaps? RETURN: without the RETURN?

intsum: lambda [
    "An Integer Sum Expressed as a Lambda"
    : "Result is the sum of first and second"
        [integer!]
    x "The first value"
        [integer!]
    y "The second value"
        [integer!]
][
    x + y
]

Hm. No, lone : is a WORD!, you could take a parameter called that and it would be legitimate.

Instead you could use #: or []: or something to give this impression of a result:

intsum: lambda [
    "An Integer Sum Expressed as a Lambda"
    []: "Result is the sum of first and second"
        [integer!]
    x "The first value"
        [integer!]
    y "The second value"
        [integer!]
][
    x + y
]

intsum: lambda [
    "An Integer Sum Expressed as a Lambda"
    #: "Result is the sum of first and second"
        [integer!]
    x "The first value"
        [integer!]
    y "The second value"
        [integer!]
][
    x + y
]

I actually think I like the []:

It hints at the absence of a word to the left of the colon, because there's no word in the frame for returning. I like how it points to whats missing, so you see it very semiotically: "this action has no RETURN, see? It's what you expected to see but aren't seeing, and we're emphasizing that your eyes do not deceive you."

In a more reduced view, with no strings:

intsum: lambda [[]: [integer!] x [integer!] y [integer!]] [
    x + y  ; want to document this will be an integer
]

Nothing's going to be perfect here, but I think I like that enough to go with it. And it solves one of my major gripes about not wanting to use LAMBDA--losing a return spec is never worth it to me for "don't have to put in a RETURN".

But this tips the scale, and I'd use LAMBDA quite often.

This looks like it's probably close to as good as it can get if SET-WORD is what we are using for RETURN: and YIELD: (I think using FENCE! for locals will sufficiently space off any locals assignments in the spec, to not have SET-WORD! at the same level)

One way or another LAMBDA needs type checking, so I went ahead and gave this a shot.

As it happens, []: is a bit more apropos than you might think. SET-BLOCK!s which receive more values than they expect will just not do the assignments of the extra values, and pass through the input as-is if no output variable is circled.

>> []: 10
== 10

>> []: pack [10 20]
== \~['10 '20]~\  ; antiform (pack!)

>> error? []: fail "HI"
== \~okay~\  ; antiform (keyword!)

So semantically, it represents a kind of transparent assignment, of something that isn't there.

It goes rather well with LAMBDA's notion of "the result just passes through" / "drops out".

[]: is annoying, but somewhat in a good way.

What I mean by that is that you are swapping between RETURN: and []:, and doing this swap makes you very conscious of the presence or absence of a definitional return inside the construct.

I think this visual sync-up is a great reinforcement of the reality of the situation. It smacks you in the face a bit: there is no RETURN, so if you say RETURN and it executes... you are returning from something else besides this LAMBDA.

I've spoken a bit before on the topic: Warts in Dialecting: When Worse is Better

The Alluring Alternative is Still Just "Leading Block"

intsum: lambda [[integer!] x [integer!] y [integer!]] [
    x + y  ; want to document this will be an integer
]

When you pick just that example, it looks less jarring. It might make more obvious sense if we standardize RETURN: and YIELD: to force them to always be first in the spec. That would make the position speak specifically to it.

But should it? I talk about the "rhythm breaking" above. Look how things are when you try putting blocks before description strings:

compose2: native [

 "Evaluates only contents of GROUP!-delimited expressions in the argument"

  return: [
      any-list? any-sequence?
      any-word?  ; passed through as-is, or :CONFLATE can produce
      any-utf8?
      null? ~word!~ space? quasar?  ; :CONFLATE can produce these
  ]
      "Strange types if :CONFLATE, like ('~)/('~) => ~/~ WORD!"/
  pattern [any-list? @any-list?]
      "Pass @ANY-LIST? (e.g. @{{}}) to use the pattern's binding"
  template [<opt-out> any-list? any-sequence? any-word? any-utf8?]
      "The template to fill in (no-op if WORD!)"
  :deep
      "Compose deeply into nested lists and sequences"
  :conflate
      "Let illegal sequence compositions produce lookalike WORD!s"
  :predicate [<unrun> frame!]
      "Function to run on composed slots"
]

To my eyes, it looks better done the other way:

compose2: native [

"Evaluates only contents of GROUP!-delimited expressions in the argument"

  return: "Strange types if :CONFLATE, like ('~)/('~) => ~/~ WORD!"
  [
      any-list? any-sequence?
      any-word?  ; passed through as-is, or :CONFLATE can produce
      any-utf8?
      null? ~word!~ space? quasar?  ; :CONFLATE can produce these
  ]
  pattern "Pass @ANY-LIST? (e.g. @{{}}) to use the pattern's binding"
      [any-list? @any-list?]
  template "The template to fill in (no-op if WORD!)"
      [<opt-out> any-list? any-sequence? any-word? any-utf8?]
  :deep "Compose deeply into nested lists and sequences"
  :conflate "Let illegal sequence compositions produce lookalike WORD!s"
  :predicate "Function to run on composed slots"
      [<unrun> frame!]
]

Although some might argue it's better to space the whole thing out, e.g. @rgchris's curl goes on like this:

curl: func [
    "Wrapper for the cURL shell function"

    url [url!]
    "URL to Retrieve"

    :method [word! text! blank!]
    "Specify HTTP request method"

    :send [text! binary! file! blank!]
    "Include request body"

    :header [block! object! blank!]
    "HTTP headers"

    :as [text!]
    "User agent"

    :user [text! blank!]
    "User Name"

    ...
]

Under this system, if that were a lambda, with a typespec for the result, you would get:

curl: lambda [
    "Wrapper for the cURL shell function"

    [text! binary!]
    "Result is BINARY! if you use :BINARY refinement"

    url [url!]
    "URL to Retrieve"

    :method [word! text! blank!]
    "Specify HTTP request method"

   ...
]

I kind of don't like the naked block, there. It's just kind of floating, without any lexical "I'm an output" signal. I prefer the []: to be there, so you at least know what to look up:

curl: lambda [
    "Wrapper for the cURL shell function"

    []: [text! binary!]
    "Result is BINARY! if you use :BINARY refinement"

    url [url!]
    "URL to Retrieve"

    :method [word! text! blank!]
    "Specify HTTP request method"

   ...
]

I'm still a bit torn, but I now feel that I KNOW that RETURN: is DEFINITELY the wrong answer.

At times I've thought "oh, what the hell, just use RETURN: and move on, people will know what it means, even though the function doesn't have 'a return'". But I'm glad to have not done that, because it's misleading and very bad.

When you realize why it's bad, and take into account the benefit of the "wart" reminding you there is no name for the return function here... I think the case for []: is pretty compelling.

2 Likes