Circling back to `with`

Another way to design something like that would be to only rebind self once at the outermost level. You can still use types to prevent invalid builds without needing an entirely new namespace at each level.

That depends on the DSL. For example, it may be creating static guarantees for certain constructions (don't let me add a <title> inside a <body>).

You should be able to do that entirely with types.

Ah, so even if self means something different at each level (e.g. HtmlBuilder vs. HeadBuilder), you imagine that only the outermost level needs the {@ … }?

FWIW, I'm not sure the sugar afforded by instance_eval-like rebinding is worth it, though maybe with an IDE and type introspection it's not so bad.

The purpose of the self rebinding is to provide a different namespace for the closure body. It only has to be one kind of thing which can provide all of the methods for all of the different kinds of HTML entities. How those things then fit together could then be managed by the type system instead of by finer-grained namespacing, which seems to easier to reason about to me and more robust.

1 Like

I haven’t given any thought to how to design a DSL like that but I agree that it sounds like a better approach. It would significantly reduce the impact of sugar such as {@ } which would be verbose by comparison in these use cases.

I'm not a fan of automatically rebinding self to the argument provided to with and then inferring it everywhere. I think the benefit are minimal for breaking consistency with other closures. It would require code like this to access the traditional self unless there is other magic introduced.

let outerSelf = self
with(v) {
    foo = outerSelf.bar
}
1 Like

That's one of the things I'm worried about. I really like self rebinding but that's a fair concern. It works fine in Kotlin because the language already has the concept of This Expression which allows you to disambiguate which upper scope you want to access.

1 Like

I don't miss the ability to redefine self, but I'd be fine with extending the the list of entities that are searched when looking for a method (which now contains self and imported modules).

That's what I'm afraid of too. I don't like the self rebinding in a sense that only self can be rebind. Why not explicitly tell which parameter should be rebind?

class SomeView : UIView {
  func foo() {
    // explicitly bind `$0` since passing a tuple to `with` can add more `$#` parameters
    UIView() with { bind $0 in 
       tag = 42               // executed on `$0`
       backgroundColor = .red // executed on `$0`
       self.addSubview($0) // self is the outer SomeView
    } 
  }
}

I like this solution a lot more than just polluting the namespace with the argument just becoming an implicit self for the scope of the closure.

I'd assume a self-rebinding feature would mostly be used to create DSL-ish APIs, and for those I think it would be better to have the rebinding in the closure type declaration, to have less syntax (in DSLs potentially repeated many times) at use point.

In Kotlin, you can declare what they call function type with receiver like this:

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

which I find rather nice, although as mentioned above, Kotlin has also solved the problem of accessing shadowed things from outer scopes.

If we had this, projects could build some very cool DSLs, e.g. adding an initializer to UIView subclasses which took a closure like this, so you could do something like:

let label = UILabel {
    text = "hello"
    textColor = .red
    font = //...
}
2 Likes

The latter is neat and such, however I once talked with @hartbit about such a functionality (without rebinding) and he quickly convinced me that this addition would be ambiguous if some other already existing init had a trailing closure.


The following idea is purely bikeshedding so please don't interpret too much in it.

I would like to see a semi-automatic rebinding which would be part of the function parameter type like inout is. In that sense the function from way above would bind the inout T (not self!!) and provide a very nice usability:

@discardableResult
public func <- <T>(lhs: T, rhs: (bind inout T) throws -> Void) rethrows -> T
let label = UILabel() <- {
  text = "swift"
  textColor = .white
}

So bind is completely optional in that sense and can only be used on function parameters. There could only be at most one parameter that can be bind. The AST printer should only print bind on closure parameters, other funtions and methods can still use this feature but exposing it from outside the module does not make sense.

// imagine this is a method of a large type.
// this should be valid
func foo(param1: bind SomeClass, param2: SomeOtherType) {
  // `param2` must stay explicit
  someMethod() // implicit `param1`
  Module.someMethod() // Resolving ambiguity
  self.doSomething(with: param1)
}
1 Like

While I see what you're going for, my initial reaction is that it's a bit too powerful for what's meant to be a convenience feature. The idea of using anything more than self for implicit lookup seems potentially confusing to me, and if you really need to access self, you can just do so outside of the closure.

The ‘anonymous methods’ idea seems a much more direct solution to this problem with is meant to solve; it's lightweight and much easier to reason about.

Other the other hand, the benefit of your solution is that it's perfect for DSLs, and as your example shows, it can be used to create a with function/operator. If this is a problem we want to solve, it does make more sense to go in this direction, and turn with into a library feature.

I think that if rebinding the implicit method lookup target is desired, self should be able to be used as an identifier as discussed here, with the exception that the implicit method lookup is the current overload for the self identifier.
eg. in

struct Example {
   func method() {}
}

struct Main {
  func method() {}
  func test() {
    method() // calls Main.method (via implicit self)
    do {
      let self = Example()
      method() // calls Example.method (via explicit self)
    }
  }
}

[editing on the iPad is terrible :-/]

I think that doesn't fit:

let number = "42"
let number = Int(number)

Why should this be forbidden, when it's allowed for self?

Your example doesn't match the way it is discussed in the other thread. They want to be able to make [weak self] strong when entering a closure. Being able to just override self by itself seems like a very bad idea; nowhere else do we allow type changes like this. With guard you can do some type changes but it doesn't force variable occlusion like this does with old self.

I am fine with rebinding self but it should only be at the start of a new scope and only when explicitly requested.

Sorry I should have linked the explicit post where it was mentioned, but didn't want to look though the thread, so I should have linked this quote directly:

Although, I see my example was indeed incorrect for my interpretation, which would indeed require the new overload for the identifier self to be in a new scope. (will edit it)
Additionally, my idea would be explicitly requested, by naming the new variable self.

1 Like

I think the way you have updated it is fine but would prefer if the syntax were more like this (tweaked from the code above):

struct Main {
  func method() {}
  func test() {
    method() // calls Main.method (via implicit self)
    do { [bind Example()] // Yes, I know this is calling a constructor and I intended it that way.
      method() // calls Example.method (via explicit self)
    }
  }
}

Ideally we could use the [bind Foo] syntax (or something similar) at the creation of any scope so you don't have to make and call a closure to do it.

My thoughts are that rebinding self should only be done via special syntax unless your are binding a weak self to self.

From my quick skimming of this thread, I don't think anyone has brought up this possible utility of a first-class with keyword yet.

An with keyword described by Xiaodi earlier, should additionally work with enum/struct/class declarations. Example:

// if we have…
enum M {
  static let a: Int = 21
  static func double(_ n: Int) -> Int { return n * 2 }
}

var mutable: Int = 0

// `with` should work in this way as well…
with M {
  mutable = double(a)
}

// `mutable` is now 42

In other words, the scope following the with keyword is a place where Self becomes implicit the way self is implicit in methods.

In practice, putting static properties under an case-less enum is the closest thing we have to a namespace in Swift. This addition will bring this use case closer to the ergonomic of an ML-style module syntax without introducing a whole new set of language features.

I mused about this a few months ago, and for this desire of mine, I'm against any introduction of with as anything short of a first-class keyword.

(side note: another thing I wish would one day exist is a shorthand to make everything in an declaration implicitly static, aka

static enum M {
  let a: Int = 42
  func double(_ n: Int) -> Int { return n * 2 }
}

…would be a sugar for M from the previous example.)

3 Likes