Arbitrary Block Evaluation

One aspect of language design can be generalized to the concept of block evaluation, which we can basically define as evaluating an expression by executing a block of statements until they return the result.

Using a block of code as an expression (typically via some kind of a function expression or closure sugar) can be useful in a bunch of situations. This is most notable in languages like Python, which lacks any mechanism for using statements in an expression (due to its reliance on significant indentation to delimit blocks).

Swift has trailing closures, which are an especially elegant solution, where applicable. However, they are not expressions in their own right (they must be preceded by an expression that accepts the closure), so they cannot (directly) solve the problem that the Multi-statement if/switch/do Expressions pitch is trying to solve.

However, we can completely remove the limitation on closures, and allow a block of statements to form an expression anywhere that an expression is allowed, without making any changes to the language whatsoever. Instead, we can simply introduce a one-line, helper function that takes a closure, invokes it with no args, then returns the result:

func evaluate<T>(_ closure: () -> T) -> T { closure() }

Note: We’re ignoring attributes like @discardableResult and @inlinable et cetera, just for the sake of simplicity.

Block evaluation inside any expression (or sub-expression) removes the problem that the pitch mentioned above attempts to solve. Multiple statements would be valid inside any expression. So, we would have no need for this new grammar:

let width = switch scalar.value {
    case 0..<0x80: 1
    case 0x80..<0x0800: 2
    case 0x0800..<0x1_0000: 3
    default: 
        log("this is unexpected, investigate this")
        then 4
}

We can just use this (already legal) grammar:

let width = switch scalar.value {
    case 0..<0x80: 1
    case 0x80..<0x0800: 2
    case 0x0800..<0x1_0000: 3
    default: evaluate {
        log("this is unexpected, investigate this")
        return 4
    }
}

The use of return here is not an issue, as in practice, we either want to return 4 from the containing function or make the switch-expression evaluate to 4, but not both in the same context (returning from a function precludes evaluating to anything).

Note: It is still possible (using evaluate) to return or evaluate, based on a predicate, within the default clause above, but using switch as an expression is syntactic sugar, so while evaluate can handle every corner case, there’s no real need to demonstrate that.

The evaluate function also removes the need for unsightly trailing parenthesis after the closing brace of immediately invoked closures. For example, the following code follows the current idiom:

var player: AVAudioPlayer = {
    let path = Bundle.main.path(forResource: "sound1", ofType: "mp3")!
    let url = NSURL(fileURLWithPath: path)
    let player = try! AVAudioPlayer(contentsOf: url as URL)
    player.prepareToPlay()
    return player
}()

With evaluate, we would just do this instead:

var player: AVAudioPlayer = evaluate {
    let path = Bundle.main.path(forResource: "sound1", ofType: "mp3")!
    let url = NSURL(fileURLWithPath: path)
    let player = try! AVAudioPlayer(contentsOf: url as URL)
    player.prepareToPlay()
    return player
}

That small change substantially improves readability by indicating to the programmer that the closure is immediately invoked (which is important context) before they read through its entire (possibly lengthy) body.

Note: Swift depends on immediately invoked closures for initializing default values inside type definitions (which cannot contain arbitrary setup code). You cannot opt out of using them.

Having a nice grammar for immediately invoked closures also promotes the adoption of other idioms that are less attractive otherwise. For example, we can use scope intercession to insert a scope between a function and its containing scope. Take the following function as an example of how this is useful:

func f(x: Int) -> Int {
    let y = expensiveComputation()
    return x + y 
}

Given that y is expensive to compute, it’s worth considering whether it changes from one invocation to the next. If not, we’d be tempted to move y outside the function, so it only gets computed once:

let y = expensiveComputation()
func f(x: Int) -> Int { x + y }

That would be more efficient, but now y has absconded its proper scope. At a minimum, we’ll need to document why it’s hanging around outside of f, and probably rename it as well (even though the shorter, original name was fine, when it was confined to its proper scope).

An immediately invoked closure can easily resolve this issue by inserting an extra scope between the function and its containing scope:

let f: (Int) -> Int = evaluate {
    let y = expensiveComputation()
    return { x in x + y }
}

There are other, similar ways that evaluate can be useful. Still, the ability to use a block as an expression anywhere that an expression is allowed is the main advantage (especially now that certain statements can be used as expressions).

So far, the examples have all used a closure that takes no arguments, as passing arguments when immediately invoking a closure is uncommon. Still, it does happen, and is something that should be supported (and something I was asked about, when discussing this topic in another thread). Overloading evaluate to take a single argument (and pass it along to the closure) is simple enough:

func evaluate<A, R>(_ a: A, closure: (A) -> R) -> R { closure(a) }

let x = evaluate(1_000) { x in x + x } // 2000
let y = evaluate("foo") { x in x + x } // "foofoo"

Note that the arguments also precede the closure now (instead of dangling off the end).

This approach can be extended to an arbitrary number of arguments by hardcoding overloads for each number of arguments. For example, this version takes two arguments:

func evaluate<A, B, R>(_ a: A, _ b: B, closure: (A, B) -> R) -> R { closure(a, b) }

let x = evaluate(1, 2) { x, y in x + y } // 3

If I understand correctly, these kinds of hardcoded variadics are exactly the problem that packs solve, so evaluate could be generalized to any number of arguments that way. In any case, passing more than one argument to an immediately invoked closure is rare, so a small number of overloads would suffice, even without packs.

Obviously, the name evaluate is open to debate as well. Personally, I’ve always named these kinds of functions iife (Immediately Invoked Function Expression), but that didn’t seem right for Swift (which consistently uses the term closure instead of function expression). In any case, in practice, the invocation will very often (but not always) be the rvalue in an assignment (as above), so any name should work especially well in that context.

Thanks for taking the time.

3 Likes

Yeah, this weird pattern:

var foo: Int { return 14 }
var oof: Int = { return 14 }()

... doesn't quite fit with the Swifty practice of making certain things very explicit, despite easy inference, a practice don't have a problem with.

In fact, I once vaguely suggested that a computed symbol ought to be written as such:

computed foo: Int { return 14 }
1 Like

In the spirit of shortened keywords like func, var, struct, etc. I'd suggest eval. And actually would be nice to have a built-in eval that would be highlighted in editors as a keyword.

I quite like the status quo, but if it starts to get annoying I would just do this:

let f = {...}

let u = f (x)

// reusable
let v = f (y)

// composable
let g = {...}

let w = f (g (w))

There you go, no need for a new addition. :slight_smile:

1 Like

@crontab - Shortening it to eval is obviously an option. I did go through all the two and three letter words, and came up with run, def and per (if I remember correctly).

I also considered express, invoke, call, define and thus, but went with evaluate, as it works well semantically as an rvalue, and just generally, and it seemed about the right length (for a function name).

I originally wanted to use a keyword too, but aimed to overload do, as a whole new keyword seemed too much to ask. This was before if and switch became expressions (kinda), so I was only trying to make IIFEs prettier at the time. In any case, you can't overload do without a new return-like keyword, for reasons I think you're already familiar with. I'd strongly prefer not having another return-like keyword, personally, and a new do-like keyword is hard to justify, when a helper function can do the exact same thing (without changing the language).

I didn't understand your comment about the status quo at all. I'm sorry. Could you copy-paste the examples from my post, and edit them to show how your solution would apply in those situations in practice, please? Again, sorry. I just didn't get it.

Because non-immediately invoked closures also start with an open curly brace? You can't tell it's invoked until you get to the parentheses.

1 Like

@Quedlinbug - A with helper may be useful in its own right. However, the example you gave doesn't really cover the usecases I laid out above (and it's kinda ugly, to be honest).

If with was able to do the same things evaluate can, then naturally, I'd be happy to just add with and use that, but I don't think it does??

While I laid out a few of the things I use evaluate for, the main benefit is permitting block evaluation within any arbitrary expression. That's the win.

Being able to use a block to express a value became more important when it became possible to evaluate if and switch blocks, so long as every branch is a valid expression.

We already have trailing closures for that kind of thing. The evaluate helper just gives them something to trail.

Do expressions are still better though, even with a new keyword (although I'd rather we just go all in on implicit return of last expression).

@Nobody1707 - Cool, but how would do-expressions be better?

I'm genuinely struggling to understand why people seem so eager to introduce new syntax and overload the semantics of existing constructs, while being so apprehensive about adding a simple helper function that does the exact same thing, in a simple way, without further complicating the language.

Three main reasons:

Do expression would type check faster. There's a lot of type checking machinery for functions that doesn't exist for any of Swift's expressions as statements. They either infer the type of the first returned value or they don't infer at all.

It's a lot easier to optimize. There's a lot of scaffolding around closure parameters that need to be optimized out that simply don't exist for exprsession statements. They're just basic blocks in the IR.

Allowing do expressions and multi-statement if and switch expressions makes the language more regular. No need to completely rewrite a single line expression into it's full statement form or a closure call just because you need a temporary variable or to log something.

2 Likes

This pitch seems to be trying to do the same thing as the multi-statement if/switch/do expressions pitch, but it doesn't really argue why this approach is the better one. I think this pitch needs a more clear motivation.

To me, this approach seems to have clear downsides compared to multi-statement if/switch/do. This can be seen in the let width = switch scalar.value examples: the bottom example's code adds evaluate { and }, which makes the code more noisy without providing any value to the reader. Additionally, the top example uses a then statement, while the bottom example uses a return statement. The then statement makes it clear that the value is being returned from an if/switch/do statement, while the return statement is more vague because return is used to return from both functions and closures.

I also don't find the "scope intercession" examples very convincing. First of all, the examples aren't realistic Swift code. Names like f(x:), expensiveComputation(), x, and y are poor names that don't describe what they represent and go against the "Strive for Fluent Usage" section of Swift's API Design Guidelines. In real-world code, I don't think that taking a constant out of a function and turning it into a static constant will be as much of a hassle as it is in the example. And if you still really want to declare the static constant inside the function, you can do so like this:

func f(x: Int) -> Int {
  enum Statics {
    static let y = expensiveComputation()
  }
  return x + Statics.y
}

A major downside to using the evaluate statement like you do in the "scope intercession" section is that you go from declaring a function to declaring a constant closure. This means that you can't use this technique to conform to a protocol's method requirements. Additionally, closures are more expensive to use than functions since closures are stored on the heap and called dynamically (unless the optimizer finds a way out of that). Closures also cannot have argument labels, which makes code that uses them less readable.

I do think that the versions of evaluate that take parameters and immediately pass them to the closure would be useful to have in Swift. There are often times where you have to create an instance and then modify it before returning it, and a function like this would help make this cleaner. I don't think evaluate is a good name for this function though, since this isn't just evaluating some code, it's passing parameters to it.

1 Like

Another idea for a keyword: get.

func get<T>(_ closure: () -> T) -> T { closure() }

let a: Int = get { // type can be omitted
	42
}

var b: Int {
	 // compiles and works as expected, i.e. computed property getter
	get {
		43
	}
	set {
		_ = newValue
	}
}

Pro: already a keyword though a "soft" one, highlighted by the editors (and markdown code blocks!)

Con: can be a bit confusing since it's also the getter for computed properties. On the other hand, on some level computed property getters are not very different from this concept.

I think I'm going to have this as a standard utility in my code :)

1 Like

Sorry, but you obviously didn't read the pitch. It explicitly makes the Multi-statement if/switch/do expressions pitch redundant. That was the primary example I gave to show why this kind of thing is useful.

The motivation is four-fold:

  • Allowing block evaluation anywhere that expressions are valid (not just inside if and switch expressions), without changing or complicating the language.
  • Moving the grammar that indicates that a closure is immediately invoked to the beginning of the closure, improving readability.
  • Moving arguments to the closure to the beginning of the closure, improving readability.
  • Removing the parens from the end of immediately invoked closures, making the idiom less ugly (people often compare the dangling parens to canine reproductive anatomy, and there are funny, but vulgar names for them (which I'm not allowed to repeat in this forum)).

It is useful to have. I've been using it with one name or another for years in one language or another. Many people do. Some languages use do as a prefix operator for this.

Using get is a very interesting idea.

You said three reasons, then provided two, spread across three paragraphs. The first reason seems to just be a general argument for always adding new grammar, because you can optimize it more.

Allowing do expressions and multi-statement if and switch expressions makes the language more regular.

That's obviously not true. It's literally a special case for a special case. Functions and closures (the only other blocks you can evaluate in Swift) do not implicitly return the value of the last expression, unless there is only one (which is how if- and switch-expressions work now).