Receiver Closures

Indeed Ruby and Kotlin don't bother about showing anything at all at the call site. This is just the way those APIs are intended to work. They don't have to draw attention with some punctuation - we're busy using them, not contemplating the language.

1 Like

Most languages don’t require potentially throwing calls to be marked withy try either. IMO one of the excellent design decisions in swift is to mark at the point of use code which has different semantics. Why should this feature be different?

You are exaggerating: with such an argument, we would not have type inference in order to preserve the huge semantic information contained in types. Or implicit optional conversion. Or implicit closure/function conversion. Etc.

I am not: I said nothing about implicit type inference or subtype conversions. Those features make perfect sense. Importantly, every declaration has a type. There is nothing magical happening here.

You did not answer the question I posed. I am still interested in an answer to that question. Silently changing the semantics of a closure is categorically different than type inference and subtype conversion. It feels much more along the lines of throwing to me. Why do you believe it is different?

Further, why should this feature be limited only to APIs which have been explicitly designed with it in mind? I already have APIs which would be absolutely perfect for use in combination with this feature. Why should I have to change them instead of just having users update their usage sites? Even more important, why should an API have to choose between “receiver closures” and normal closures? Making this worse, overloads to support both if necessary doesn’t seem to be a good option without any syntax at the call site. This seems likely to lead to confusing compiler errors.

1 Like

Because of good taste, I'm afraid ¯\_(ツ)_/¯

I don't want to be dragged into your debate. You, please, think about prior art.

Have a nice week-end!

This isn’t my debate. It’s a design discussion about a proposed feature. I asked a question with an open mind. I am honestly interested in hearing perspectives about why this is different.

If you’re not interested in continuing that’s your choice, but claiming better taste is not going to change anyone’s mind. Have a nice weekend!

FWIW, having written a lot of DSLs using this feature in Ruby in the past, I am quite familiar with the prior art in this area. I loved this feature when I was using Ruby.

But I think we need to consider the best design in the context of Swift when we import features. It isn’t clear to me what that is yet, but I think there are good reasons to at least consider usage site syntax here. Swift is vastly different than Ruby and despite common perception otherwise, there are also many significant differences from Kotlin. This differences must be taken into consideration.

2 Likes

This approach is interesting, and I like it better than the alternatives so far (clearer while still concise, more flexible because it can be applied to any closure). I think the natural spelling would be

let website = html ${
    head ${ title = "Awesome Title" }
    body ${
      div ${ text = "div1" }
      div ${ text = "div2" }
    }
}

as an analogy to the shorthand argument names ($0, etc).

3 Likes

I hadn’t thought of aligning the sigil with implicit $ arguments but makes perfect sense. I like jut a lot! +1.

Agreed, there is a really nice symmetry! It still has the brevity that you get from some other languages, but much increased clarity that something is different. +1 to the $ design

@anreitersimon That spelling is great. I like how it fits into a bigger mental model.

Having said that, I regrettably have a few points against the syntax in practice:

An argument (IMO) against Kotlin's receiver-application syntax is that there is the question of whether ad-hoc extensions are allowed as so:

let method = makeReceiver() // -> MyType.(Param) -> Result
myValue.method(param) // Does this work?

This seems confusing to me, so I'd prefer the self value to be passed in as a parameter at the call-site: method(myValue, param)

However, this works in favour of keeping the distinction out of the type-system. Looking at the discussion so far, this could be by including a parameter-only @receiver attribute, or making the decision part of the literal, with $ or @.

The type-system mental model also risks breaking down when you consider that in a world with type-system-level receivers, unapplied methods should technically use this receiver type, as the function definition binds a value of self:

let unapplied = MyType.method
// What's the type of `unapplied`?
//
// Currently:
//   (MyType) -> (Param) -> Receiver
// We have an unimplemented proposal for:
//   (MyType, Param) -> Receiver
// With this proposal, should technically be:
//   MyType.(Param) -> Receiver



On another subject, it seems like the decision of whether a receiver should be a closure makes sense as an API contract, rather than a convenience feature.

Firstly, looking at the use-cases, if you want to rebind self, you always want to rebind self:

// Good
html {
  head { title = "Hello world" }
  body {
    div { }
  }
}
// Confusing
html {
  $0.head ${ title = "Hello world" }
  $0.body {
    $0.div ${ }
  }
}
// Abominable
let flattened =
  zip(array1, array2)
    .flatMap ${ [self, $0] }

As you can see, defining things at the API level would solve most misuse of the feature.

Secondly, Swift's direction more generally is to make things part of the API contract where it would help improve clarity. We see it again and again with structs vs classes, argument labels, let vs var, and @escaping, to name a few, all of which improve code clarity tremendously. Avoiding the issues outlined above would also help improve clarity, for me.

Aside: Looking instead for prior examples of a symbol changing the semantics of a literal, as the ${ } syntax does, we find raw string literals, binary/hex number literals
 But closures as a whole don't feel like a ‘real’ literal, to me, any more than function declarations are literals.


A major goal of this feature is to provide a clean way to provide DSLs, which means minimising invasive and redundant language syntax. Functions constituting DSLs will be documented as such, and have some visible attribute in their function signature in autocomplete, and possibly Xcode's placeholders. Yes, it adds surface area to the language and must be learned, but ${ } isn't any better in this regard, while being much less searchable than an @receiver attribute.

2 Likes

@MutatingFunk
I am also a bit skeptical towards directly being able to call a receiver closure on a instance.

I think providing a way to make it clear that something is called on a receiver is important (even if its optional)

One way mentioned somewhere in this thread is:

let method = makeReceiver() // -> MyType.(Param) -> Result 

@receiver.method()
// or maybe even
self@receiver.method()

I think receiver closures have some analogies to KeyPath which i briefly mentioned in the original pitch, but i am bringing it up because i think it has some potential.

The similarity is that they both require a Base/Receiver to be called. (something to be called on)
This could also be extended to how a ReceiverClosure is called.

struct MyType {
   let aInteger = 1
}

// KeyPath usage:
let keyPath = \MyType.aInteger // -> KeyPath<MyType, Int>
//called like 
let _ = myValue[keyPath: keyPath]

// ReceiverClosure usage:
let method = makeReceiver() // -> MyType.(Param) -> Result 

// converts a `ReceiverClosure` into a normal closure called on myValue.
let normalMethod = myValue[keyPath: method] // (Param) -> Result

myValue[keyPath: method](param)

// Note: "keyPath" might not be the best wording for the argument label

However this will not be nice to use if you try to call something without assigning it to a variable first, forcing you to write a closure inside the subscript.

myValue[keyPath: { p in 
   print(self, p) 
}](param)

I am not sure if this is a big problem.


As for the call site, i am in favour of not requiring different syntax.
I think even though its shorter it still adds noise.
But thats just my personal preference.

1 Like

Why not just allow this directly with KeyPaths?

\MyType.aMethod // KeyPath<MyType, (MyType, Param) -> Result>

// Potentially:
\MyType.aProperty.aMethod(42, MyInstance()).anotherProperty // KeyPath<MyType, AnotherType>

I find “can I deliberately write confusing or terrible code using this” to not be a helpful way to evaluate a feature in general. Almost every feature in Swift can be made to look bad if you're motivated to (e.g. if you wanted to make shorthand argument names for closures look like a bad idea then you could show an example with a lot of arguments, intermingling $0
$6), so I think it's fairer to try to predict how a feature would be used in practice by developers of various skill levels.

1 Like

How does that saying go? "You can write Perl in any language."

1 Like

As you say, potential for misuse is not ‘a helpful way to evaluate a feature’, but I think it's worth pointing out, as it's one of the few objective points we can make on any proposed syntax.

But perhaps this does just come down to personal beliefs and preferences. I believe that @receiver ‘better fits the direction of Swift’ for reasons I've already given, and expect that it will be more accessible with developers being introduced to the feature through documented functions such as with(_) { } or html { }, rather than an ad-hoc $ symbol.

Evidently, your preferences and expectations aren't aligned to mine, but for what it's worth, your proposed ${ } syntax is my favourite so far for that version of the feature. If we go with call-site annotations, we have no choice but to trade off discoverability, to succeed in minimising redundant code for DSL use-cases. The reasoning behind the symbol makes it the most accessible option available, IMO.

1 Like

Redefinition of self could imho be extremely confusing - especially when old and new self share some methods.
It enables some niceties, but many of them don't actually need the redefinition, because they don't need self written out:
Instead, it's the option to call methods of self while skipping that word.
The same effect could be achieved by manipulating how method lookup is performed (like import does).

This might still cause confusion, though ;-)

Sounds like Pascal's with clause.

1 Like

If the primary use case is something like a with clause, what about this alternate syntax?

var person = Person()

person.{
    givenName = "Fred"
    familyName = "Rogers"
    sayHello()
}

By using person.{}, anything inside the {} implicitly refers to person as self. I don't believe this syntax would conflict with any existing code, and seems more obvious to me about what is going to happen.

Of course, this design doesn't address something like where you want an API to pass a different self than the obvious one, but I'm not convinced implicit rebinding is a good idea there anyway.

// Supporting the following API seems like a bad idea to me:

contactBook.contactPeople(withFamilyName: "Rogers", handler: {
    // This is actually a receiver closure, where 'self' is a 'Person'.
    // But that's really not obvious unless you look into the API
    // docs. It seems like 'person' should be an explicit parameter here.
    self.sendMessage("Hi Mr. Rogers")
}
3 Likes

I understand the ‘fear’ of not having a visual clue at the call site but tbh that’s the whole point of it. I feel like any required syntax at the call site will reduce the value of a language change, $0 is already succinct enough.

4 Likes

Seems very readable. This would still enable hierarchical DSLs like html { } (or rather html.{ }) when combined with the callable types proposal currently in review. Here's an impression of how the API declaration might look:

// Receiver closures

struct HTML {
    var head: Head // Might need a name which doesn't collide with the DSL method

    mutating func head(_ constructor: @receiver (inout HTML.Head) -> ()) {
        var head = Head()
        constructor(&head)
        self.head = head
        // Or just: `constructor(&self.head)`
    }

    struct Head {
        

    }
}

// With-style constructor blocks

struct HTML {
    var head: Head // No boilerplate!

    struct Head {
        

    }
}

I think the best part of this is that a constructor block would actually be a useful feature independently of DSLs, and this syntax fits that use-case perfectly. The limited room for abuse is also nice plus.