Immediately invoked named inner functions

Currently, Swift supports immediate invocation of anonymous closures (they are also called "self-executing closures"):

let x = 0
{
  print(x)
}()

Also, there are inner functions, but they need to be called in a separate statement:

let x = 0
func myFunction(x:Int) {
  print(x)
}
myFunction(x)

In this post, I've presented the case for immediate execution of named functions with shorthand parameter passing syntax:

let x = 0
run func myFunction(x) {
  print(x)
}

With some support from the editors and tooling, this should be a useful tool to manage the complexity of large functions, without leaking the complexity to the containing source code file or class, and with some additional benefits. See the linked post for more details.

1 Like

Can you elaborate more on this? If they're immediately executed, then what's the purpose of them being named? Is it just for the reader's benefit?

How does it compare to the do syntax (used for introducing and ending a new nested scope)?

do {
    let x = 123
    print(x)
}
print(x) // 💥 error: use of unresolved identifier 'x'
4 Likes

Please read the linked post, it's entirely devoted to the elaboration and argumentation for the idea. In short:

  • Auto-folding with preserving semantic information
  • Forcing giving names (a form of comments)
  • More structure and protection (the variables from scope can't be captured unless explicitly passed)
  • Names appear on stack traces
  • Etc.

Ooo I really like that.

I'd be pretty against any syntax that has the form func functionName(variableName) where there is no explicit typing on parameters. It seems to break from the established rule that func declarations must have full typing in the signature, whereas anonymous functions can participate heavily in type inference.

But generally, I'm struggling to see how this is better than what we can do today.

3 Likes

the variables from scope can't be captured unless explicitly passed

This isn't how local functions work today, so I feel like we'd want to tackle that if we think it's important, and separate it from "run-immediately" as a concept. Otherwise people might abuse run-immediately in order to get isolation, but immediately then produce a closure, which would subtly behave differently than just making the closure in the outer function body directly.

8 Likes

I personally use do blocks, inner functions and closures pretty often but haven't had the need to immediately run a name function, or maybe it just doesn't annoy me.

Without wanting to pollute this thread with that topic, having a way to disallow capture on closures (or inner functions) is something I often felt it would pretty useful. Maybe an empty capture list [_] would suffice.

2 Likes

Having inner functions capture state is my primary reason for using them. I can't say I've ever wanted inner functions that specifically didn't capture anything.

3 Likes

This isn't how local functions work today, so I feel like we'd want to tackle that if we think it's important, and separate it from "run-immediately" as a concept.

Capturing, named inner functions which can update the state of the containing function (and call themselves recursively) turn functions into mini-objects. They build up the complexity. It's not necessarily bad because the complexity may be necessary, not accidental.

Run-immediately is largely a way to structure code and restrict complexity of existing functions which don't need this local capturing feature (mini-object-ness) functionality, or already has it in some different points.

So, IMO the fact that inner functions do capture doesn't contradict the idea that run-immediately may not capture. They are different tools for almost opposite tasks, in some sense. I would not call people "abusing" run-immediately to get isolation, I would call it "using the right tool for the job".

Perhaps, the confusing thing is that they are both called "functions". Maybe run-immediately shouldn't be called "functions" at all, but rather "blocks", for example:

block myFunction(x) {
  print(x)
}

Commentators to the post on Medium pointed to a very valuable idea (IMO) that it should be possible to test run-immediately functions. Currently, it's not possible to test inner functions in isolation from the containing functions. To make it possible to call run-immediately functions from the unit test code, they must not capture. Syntactically, extracting run-immediately into a function of the same level as the containing function must always be possible and trivial, apart from attaching the inferred parameter types.

IMO, the value of that is questionable. Inner functions are essentially an implantation detail. Testing them would be like trying to test individual lines of code inside a function. Both difficult and of little value.

6 Likes

What does the stack trace look like if there's a crash inside an inner function? Could it be that adding a name, even to an inner immediately invoked function helps clarify where a crash happened when looking at the crash logs? That could be a reason to name one.

Consider that this might be the result of your experience of working with code where everything that you want to test is already extracted as functions because they need to. With testable run-immediately functions, many small utility or supporting algorithmic functions may not need to escape the containing function.

Inner functions are invisible outside the enclosing function. There's no way to test them without testing the outer function.

Yes, but it shouldn't necessarily be so for run-immediately.

it pains me to be the one to say this but static methods get you most of the way to what you seem to be after. they aren't inline, but they tick off all of the other boxes. I agree with Jordan that pure is an interesting and worthwhile thing by itself but this feature, as described, doesn't make up for the complexity it would add.

3 Likes