Actor seems not work with ExpressibleByIntegerLiteral

@frogcjn Yes, that will work. But I’m really trying to avoid needing the associated type to be Sendable, as that is a severe constraint for my situation.

@x-sheep This is a good question! In general, no. An async function requirement is pretty flexible and you can still use isolated functions in a conformance. However, this was along the right line of thinking.

The protocol I had was designed in a way that is just fundamentally hard to use without modification. To be callable, it has to be safe to move a type of Value into the use. Making it Sendable is the easy route. But there are other ways.

protocol AsyncRequirement {
  associatedtype Value

  nonisolated func use(_ value: sending Value) async
}

actor UsesValue: AsyncRequirement  {
  // error: Non-Sendable parameter type 'Self.Value' cannot...
  func use(_ value: sending Int) async {
  }
}

Today, the compiler also rejects this. And I think that really drives home the point that the build-time validation happening here is not considering the sendability of the arguments at all.

Here’s what I actually settled on:

protocol AsyncRequirement {
	associatedtype Value

	nonisolated(nonsending) func use(_ value: Value) async
}

actor UsesValue: AsyncRequirement  {
	nonisolated func use(_ value: Int) async {
		await internalUse(value)
	}

	private func internalUse(_ value: Int) {
		// ...
	}
}

This code is acceptable because the stage of validation happening believes (correctly) that the value cannot be transferred across isolation boundaries. But, then internally a transfer is exactly what I do.

I then got thinking even more high-level about this. Not only is my particular implementation safe, I think even more general implementations could also be safe.

class NonSendable {}

protocol AsyncRequirement<Value> {
  associatedtype Value

  nonisolated func use(_ value: sending Value) async
}

actor UsesValue: AsyncRequirement  {
  // error: Non-Sendable parameter type 'Self.Value' cannot be sent...
  func use(_ value: NonSendable) async {
  }
}

func test() async {
  let ns = NonSendable()
  let a: any AsyncRequirement<NonSendable> = UsesValue()

  // transferred via RBI
  await a.use(ns)
}

I think it is possible that this particular compiler error is not just inappropriate for Sendable types, but for all types. Because it could be the case that a value is successfully sent, and it would only ever be knowable at a particular callsite.

But my understanding of protocols is limited and it could be there are situations I’m not thinking of.

(I’m also not 100% sure that my contrived example here correctly demonstrates what I’m trying to say. But I think it at least gets the general idea across.)

This generally requires the conformance to be written as actor UsesValue: @isolated AsyncRequirement. I don't know if this works when the protocol uses nonisolated on the function declaration.

I think you are thinking of the isolated conformance feature. That works only with global actors, which is kind of the inverse of what you have here. (you cannot use @isolated like this in a conformance)

Here’s what I’m talking about:

protocol AsyncRequirement {
	nonisolated func somework() async -> Int
}

actor MyActor: AsyncRequirement {
	// this compiles despite being isolated to `self`
	func somework() async -> Int {
		0
	}
}
nonisolated
actor ActorInt : ExpressibleByIntegerLiteral {
    private(set) var value: Int
    init(integerLiteral value: Int) {
        self.value = value
    }
    static func +=(lhs: isolated ActorInt, rhs: Int) {
        lhs.value += rhs
    }
}

This works also in Swift 6.2.4 with Xcode 26.3

The best workaround I can come up with is this:

protocol AsyncRequirement {
	associatedtype Value

	nonisolated func use(_ value: Value) async
}

protocol Refined: AsyncRequirement where Value: Sendable {
    nonisolated func use2(_ value: Value) async
}

extension Refined {
    nonisolated func use(_ value: Value) async {
        await use2(value)
    }
}

actor UsesValue: Refined  {
    typealias Value = Int

	func use2(_ value: Int) async {
	}
}

It still uses the kind-of "refined protocol trick", but we need an explicit "jump method" here. And I agree such kind of solution is still very fragile.

2 Likes

The behavior here is surprising, and I think we need a more thorough investigation of what's going on. There have certainly been surprising language interactions with concurrency that we needed a proposal to fix, but this might just be a bug in the typechecker.

3 Likes