[Pitch] `With` functions in the standard library

I've linked to the pitch here.

Do people want this in the standard library? This is my first pitch.

10 Likes

I'm not sure what would be the best name, but I have a similar function called modify in many of my projects. One important use case you're not mentioning is modifying the value at the other end of subscript or some other kind of chained expression:

var dict = ["someKey", [1, 2, 3]]
modify(&dict["someKey"]) { value in
   switch value?.count {
   case 2...: value!.removeLast()
   default: value = nil // remove entry for empty array
   }
}

Here you can resolve the chain to the subscript only once while making a change to its value. Otherwise you'd have to write this:

var dict = ["someKey", [1, 2, 3]]
switch dict["someKey"]?.count {
case 2...: dict["someKey"]!.removeLast()
default: dict["someKey"] = nil // remove entry for empty array
}

Edit: On second reading, I realize my modify function isn't the same thing as what is proposed here. You are returning a modified copy of the value whereas my function takes the value inout and changes it in place. I guess they are different things, although a bit related similar to how reduce(_:_:) and reduce(into:_:) are related.

1 Like

That could be another with overload.

@inlinable
@discardableResult
public func with<T>(_ value: inout T, transform: (inout T) throws -> Void) rethrows -> T

@inlinable
@discardableResult
public func with<T>(_ value: inout T, transform: (inout T) async throws -> Void) async rethrows -> T

+1 from me. Personally I’m a big fan of with; it helps structure code that has to do lots of setup (e.g. code working with Metal and setting up render passes), and in general I like the prospect of leaning into the functional programming side of swift a bit more (although I don’t know if everyone does).

The other main benefit I like is that it allows the variable itself to be immutable, which makes sense for a variable of that nature (and makes it clear that it gets setup once and then left alone, which isn’t always clear otherwise and I’ve had a bug caused by something like that).

I think the name with is good because that’s used in other languages/libraries already, and we already have the precedent of withUnsafePointer, withContinutation, etc

6 Likes

No strong opinions on the name or if this is something worth including, but I too have written this exact helper function and found it useful.

1 Like

I've wanted something like this for a while, but a more correct implementation would be:

@inlinable
func with<T>(_ object: consuming T, update: (inout T) throws -> ()) rethrows -> T {
    try update(&object)
    return consume object // I'm not sure that this consume is required.
}

For the same reason that parameters to initializers default to consuming.

2 Likes

I too have this exact function (with the same name) in my own personal library. It would be great to have it in the standard library.

2 Likes

+1. Previous discussions:

5 Likes

I think this is one of these cases, where a lot of people come up with similar extensions in their projects to fulfill a similar need, so its probably worth thinking about if this warrants a language level solution.

In terms of alternatives to with here is a solution that similar to the one @anon9791410 proposed uses a dynamicMemberLookup, callAsFunction and a postfix operator / to achieve a syntax like this:


// Given

struct Starship {
  var commandingOfficer: String
  var registry: String
  var isActive: Bool
}

extension Starship {
  static let titan = Starship(
    commandingOfficer: "William T. Riker",
    registry: "NCC-80102",
    isActive: false
  )
}

// Application Example

struct TVShow {
  let titanRefit = Starship.titan/
    .commandingOfficer("Liam Shaw")/
    .registry("NCC-80102-A")/
    .isActive(true)
}

// or

struct TVShow {
  let titanRefit = Starship.titan/ {
    $0.commandingOfficer = "Liam Shaw"
    $0.registry = "NCC-80102-A"
    $0.isActive = true
  }
}
1 Like

+1

For me, this is a long wanted feature. I really like Dart's cascade operator - the ergonomics/expressiveness of writing and using it lead to me prefer that spelling.

2 Likes

Regarding the naming, if we have an inout variant I think it should actually have another name. And to me what would more closely follow the naming guidelines is something like this:

// modify in place
modify(&value) { $0.x = 2 }

// returns modified value
let newValue = modifying(value) { $0.x = 2 }

Overloading with to mean both of these would be confusing I think.

3 Likes

Well, in both method variants you're doing something with the value, but they do have different semantics.

While that looks pretty cool in practice, I feel like the syntax is non-obvious, and it's less flexible; you can't call methods on the value or access variables with throws or async getters.

1 Like

For us this concept is extremely useful. The way we actually implement it though is with the following operator:

precedencegroup FunctionApplicationPrecedence {
  associativity: left
  higherThan: BitwiseShiftPrecedence
}

infix operator &>: FunctionApplicationPrecedence

public func &> <Input>(
  value: Input,
  function: (inout Input) throws -> Void
) rethrows -> Input {
  var m_value = value
  try function(&m_value)
  return m_value
}

The operator is spelled &> because it represents the "inout" version (thus the &) of the common "pipe" operator |> (which we also use).

The usage of this operator has greatly improved our code and made us more productive, even in unexpected ways. For example, if we need to only append some values to an array if a condition is true we write something like this:

return [1, 2, 3] &> {
  if shouldInclude4 {
    $0.append(4)
  }
}

I would definitely support any proposal that added both |> and &> to the standard library. I'm not a fan of the with free function though, because it requires writing with before the value that's going to be mutated. Also, I don't like the extra parentheses. In absence of scope functions à-la Kotlin, I think an operator is a better solution.

8 Likes

I'll add these operators to Alternatives Considered.

Interesting: your inout version is about whether the closure takes an inout parameter or not. But my inout version is about whether the value is passed inout or not. We have a 2×2 grid of inout possibilities there.

An alternative syntax that feels potentially more idiomatic could be to use result-builder-style / SwiftUI-style method chaining:

// Instead of using a closure:
let components = with(URLComponents()) { components in
  components.path = "foo"
  components.password = "bar"
}

// We could use method chaining:
let components = URLComponents()
  .path("foo")
  .password("bar")

It would certainly be nice if there was a way to support this pattern without having to manually implement the method for each corresponding property. I suppose a macro could be created for this, but it would be really nice in my opinion if this were possible on all types without having to modify the source of the type to adopt an annotation or anything.

3 Likes

The operator is designed to specifically return something that should be passed to the next expression (usually a returned value, or a value passed as argument to a function), hence the > part: if it's only about mutating an existing value with inout, I think the operator should look like an assignment, so something like

func &= <Input>(
  value: inout Input,
  function: (inout Input) throws -> Void
) rethrows {
  try function(&value)
}

The function isn't really useful if it doesn't take an inout parameter; what would you do with that?

If this was possible, Kotlin-style generic scope functions would be the best solution, in my opinion.