SE-0293: Extend Property Wrappers to Function and Closure Parameters

Thank you for your comments, and I see how unapplied references can be a major concern for this proposal. However, I think that the changes proposed do match the current behavior API author expect. For one, consider a library that manages tasks. The idea is simple: we have a task dictionary and we just add our tasks at the user's command, and then somehow initiate this task (we don't care about the last part):

// However, we have an implicit parent task at ID 0,
// so we’ll implicitly raise the provided tasks' IDs by 1.
addTask(withId id: Int) {
  let adjustedId = id + 1 ⚠️

  tasks[adjustedId] = … 
}


addTask(withId: Int.max) ❌
// Shouldn’t the library handle this case explicitly?

Furthermore, while we explore integer overflows, let's examine some examples with floating points. Imagine a drawing library that has some built-in drawer types, such as rectangles, circles, triangles, etc:

struct Rectangle : Drawer { 

  let width, height: Double
  
  init(width: Double, height: Double) { ... }


  func draw(to canvas: Canvas) {
    canvas.addRectangle(width: width, height : height) ⚠️
    // Are we sure these are safe.
  }
 
}


let canvas: Canvas = ...

let rectangle = Rectangle(width: .infinity, height: .infinity) 

canvas.draw(rectangle) ❌
// Shouldn't the library catch this and handle this explicitly,
// instead of continuing with undefined behavior?

Let's also examine class references, as you also mentioned them in your post. Consider this furniture-related library that allows us to create furniture instances and to propose their estimated price:

class Material {

  … 

  func makeMoreShiny() { … }

} 

struct Furniture {

  let name: String

  let material: Material ⚠️
  // This is a reference, and it is not 'protected'
  // to ensure that when we create a furniture,
  // its material remains constant.

} 


let wood: Material = … 
// This is a reference.

let plainDesk = Furniture(name: “desk”, material: wood)


var premiumWood = wood
// This is also a reference.

premiumWood.makeMoreShine() ❌
// Now our plain desk has premium wood.
// This obviously not desirable, as creating furniture 
// and then seeing it having changed without
// understanding why, is bad API design.

Of course, saying "oh look, the language already has things library authors should be worried about, let's add some more" is a bad approach to evolving a language. However, library authors also need custom features (atomics, unsafe memory management, etc.). Therefore, I think that unapplied function references should remain in the proposal, because not doing so would seem as too much of a limitation. An example of why a library author may benefit from unapplied references is when wanting to provide some behavior through a closure, while also providing an easier-to-use function API:

@propertyWrapper
struct Lowecased {

  var rawValue: String


  init(wrappedValue: String) { ... }

  var wrappedValue: String { ... }

}


protocol Task { ... }

struct EffiecientTask : Task {

  func changeName(@Lowercased to newName: String) { ... }

}

func addTask<SomeTask : Task>(_ task: SomeTask) {
  nameManager.add(task.changeName)
  // Here, 'nameManager' may need to have access to the 
  // raw value ofthe provided name, which _is_ provided
  // by the backing wrapper type.
}

In the above example, should the unapplied reference of changeName(to:) take a String or be banned, library authors would not benefit. In the contrary, they would need to separate the function into private and publicly-exposed ones or resort to other workarounds, just to avoid checking that their assertions are valid.

I hope I've addressed your concerns!

Filip

1 Like