Hi, I have a function returning a type, the type has several special methods. The core requirement is that I want the users of this function to always remember invoking one of the special methods before the returned value go out of scope.
My attempt is this: model this type as noncopyale, then mark those special methods as consuming, and at last rely on runtime checks in deinit:
struct MyResource: ~Copyable {
var isConsumed = false
// a *normal* function
func read() -> String { "" }
// a *special* function
consuming func delete() {
/* ... do something ... */
isConsumed = true
}
// a *special* function
consuming func saveToFile() {
/* ... do something ... */
isConsumed = true
}
deinit {
assert(isConsumed)
}
}
func sdkAPI() -> MyResource { MyResource() }
func user() {
let resource = sdkAPI()
if Bool.random() {
resource.saveToFile()
} else {
return // runtime crash
}
}
What is your way to achieve this goal, is there any techniques that rely on -- not runtime, but -- compile-time features?
IMO your approach is a bit far-streched. Since there is no language feature to support that behavior, I think you might rely on API design. Suppose this is your original code:
How about implementing a wrapper function and expose it as API?
extension Resource {
enum Operation {
case delete
case save
}
}
func foo(value: Int, op: Resource.Operation) {
let res = _foo(value: value)
switch op {
case .delete:
res.delete()
case .save:
res.save()
}
}
It's simple and clear and works at compile time. It would be ideal to implement it using macro, though I doubt if it's feasible in practice.
EDIT: if delete() and save() take parameters, you can pass them using associated values.
EDIT2: Python has a nice feature called context manager, which is often used to manage resource. In Swift it's typically implemented as API. There are lots of examples in stdlib, though I can't think of one off the top of my head.
Thanks for your reply, but in my opinion that does not answer the general-purpose question being asked. What is needed here is kind of a reserved version of definite initialization analysis: definite consumption.
For your example, how can we prevent users from forgetting to call either foo(op: .delete) or foo(op: .save)?
You can use discard self in a consuming method to indicate that self has already been destroyed and the deinit does not need to run. That will let you fatalError in deinit without tracking dynamic state:
struct MyResource: ~Copyable {
// a *normal* function
func read() -> String { "" }
// a *special* function
consuming func delete() {
/* ... do something ... */
discard self
}
// a *special* function
consuming func saveToFile() {
/* ... do something ... */
discard self
}
deinit {
fatalError("not consumed")
}
}
Without language support for must-consume types, this is the best you can do.
And note that even if we got language support for must-consume types, you could always leak such a type:
struct TryingToBeLinear: ~Copyable {
consuming func finalize() { ...; discard self }
deinit { fatalError() }
}
func naughty(_ x: consuming TryingToBeLinear) {
final class B {
var other: AnyObject!
}
final class A {
let x: TryingToBeLinear
let b: AnyObject
init(_ x: consuming TryingToBeLinear, _ b: AnyObject) { self.x = x; self.b = b }
}
let b = B()
let a = A(x, b)
b.other = a
// no crash
}
You could make TryingToBeLinear also ~Escapable to avoid this possibility, I think.
It isn't foolproof, but one of the rules that a strictly-linear type would need to follow would be that any class that owns such a value would need to explicitly consume the value in its deinit. That wouldn't prevent you from leaking such a value by putting it in a global object or entangling it in a reference cycle, but the behavior would at least serve as a signal that it won't "just work". (Though more generally, your process can still crash or exit before the end of the value's lifetime even disregarding such leaks, in which case any cleanups also won't run.)