I'm not familiar with the concept of monadic type, so upfront apologies if this answer is irrelevant to your question ;-) I think it should be possible to have safe v unsafe closures, e.g.:
typealias SafeProc = (Int) /*safe*/ -> Double
typealias UnsafeProc = (Int) unsafe -> Double
/*safe*/ func safeCall(Int) -> Double {...}
unsafe func unsafeCall(Int) -> Double {...}
var a: SafeProc = safeCall // no brainer
var b: SafeProc = unsafeCall // 🛑 Error
var c: UnsafeProc = safeCall // ✅ ok
var d: UnsafeProc = unsafeCall // no brainer
sorry for the FP jargon. All I'm really saying is that I can convert (for example) any throws function to a function that returns a Result instead of throwing. Similarly for async and Task. Both Result and Task already have inits for this. Here's the throws version as an example of what I mean:
In this example Result is the generic type that precisely serves the purpose of converting a throws-colored function to an uncolored function. if we are going to mark functions as:
func myUnsafeThing<T, U>(_ t: T) unsafe -> U
I just struck me that there could be a similar type.
I suppose that I should point out the fly in the ointment here is that composition of function color decorators leads to conflicts with already locked-in types. For example, a choice that could have been made with Task would have been to have a single generic parameter for the type to be delivered in the future rather than incorporating a second generic param for the error type. i.e. Task<T, Swift.Error> could have been implemented as Task<Result<T, Swift.Error>> and Task<T, Never>could have been simply Task<T> but that now they can't.
(btw, this explains why you must say async throws and can never say throws async. async throws becomes Task<Result<T, Swift.Error>> where throws async wold be Result<Task<T>, Swift.Error>)
Anyway, adding another effectful color decorator for unsafe seems to me as if it would pose difficulties of composition with throws and async.
While unsafe can be considered an effect, I don't see any reason to integrate it with the type system to the degree of throws and async. There's no practical reason for code to be polymorphic over unsafe-ness the way rethrows/reasync provide, or to try to capture an unsafe computation in some kind of monadic wrapper, or propagate unsafe-ness into function types beyond first-order declarations. Allowing that sort of abstraction also seems to me to be an anti-feature, since the purpose of these modifiers is to guide auditing, and abstraction gets in the way of understanding what code actually does, and composing unsafe code through high-level abstractions is a common way that C++ codebases become impenetrable. unsafe also has no ABI or representation impact on the code, so it can be freely added or removed to existing declarations. First order effect propagation is all we really need IMO.
I would also say that this wasn't really why we fixed the effect order. We only did so to avoid style wars and avoid the suggestion that async throws and throws async might mean different things. Effect "colors" are an unordered set, not a strict nesting like monadic wrappers are; you can do throws-only things or async-only things within an async throws function without having to "lift" or invert the computation through the other layer.
I want to think about that. To me, it seems eminently desirable (though admittedly maybe not practical from a compiler-writer's standpoint) to be able to use the type system to provide strict compile time guarantees that my unsafe code is isolated from my safe code except for specifically provided bridges back and forth. Sort of in the same way that AsyncStream is provided precisely to allow me to bridge from my synchronous code into my async code in a very strict manner which preserves ordering.
I'm not sure what form this would take for a general unsafe effect mechanism (I imagine there's a ton of reading I'd need to do) though I can understand that this may not be everyone's vision for such a feature.
That's really interesting. I would not have thought that it basically came down to a coin toss on the ordering, given that what emerged seemed to match the monad transform-ed form so naturally.
I think the unsafe { } block is the way you do that, and there's little value to letting unsafety propagate through abstractions. If we did have unsafety polymorphism, then for example you might declare map as (simplifying away the Collection genericity):
unsafe {
foos.map { x in withUnsafePointer(x) {...} }
}
instead of
foos.map { x in
unsafe {
withUnsafePointer(x) {...}
}
}
which doesn't really help understanding what parts of the code are unsafe. map doesn't contribute to the unsafety of the code, it's only the body of the closure being passed to map that's unsafe, so including it in the unsafe block is unnecessarily expanding the scope of the code that's allowed to do unsafe things.
The way I would do that is to make it so that the reference to the closure has to happen in an unsafe block, since that is where the unsafety is being introduced into the expression, and where a developer has to look to examine it.
You can also think about this from an auditor's perspective. If someone sees the declaration
func foo(_: () unsafe -> Void) {...}
and an unsafe closure is allowed to contain unmarked unsafe code, then that means they have to hunt down every use of foo in addition to every unsafe (or however it's spelled) block and function in order to complete their unsafety audit. I don't think they'd appreciate that.
func foo(execute: () unsafe -> Void) {...}
let x = {
unsafeCall() // 🛑 error, making unsafe call from a safe closure
}
foo(execute: {
unsafeCall() // 🛑 ditto error, making unsafe call from a safe closure
})
foo(execute: { @unsafe // similar to @MainActor
unsafeCall() // ✅ ok
})
only those usages of foo that are getting passed unsafe closures... this could be significantly fewer than "every use of foo".
This brings us back to the idea of having explicit "unsafe xxx()" at the use site, similar to how we do it with "try".
"Too noisy" can actually be a great motivator... to keep "unsafe" code small.
unsafeCall should only be referenceable in a call expression inside of a marked block, yeah. But if that's the case, then there's minimal benefit to propagating the unsafe-ness through the closure type, since the unsafety has already been called out at the source.
If foo can accept closures with unsafe code, the only way to tell which usages of foo get passed unsafe closures is to look at all of them.
The question is how do we make the differentiation easier to spot by the reviewer...
For example:
// we need to look at this one:
foo(execute: { @unsafe
bar()
foo()
baz()
}
// we could skip this one:
foo(execute: {
bar()
baz()
}
// and that could be a compilation error:
foo(execute: {
bar()
foo() // 🛑 that's unsafe call
baz()
}
And if we agree that we need that, then I think the contains_unsafe_code { } block is an existing marker that does that adequately. When the unsafe code is already contained that way, there's not much value in propagating the unsafety out to the closure type.
I see your point, that's one of the possibilities.
IMHO, this one is not bad either (and is in line with the "try"):
unsafe foo(execute: { // similar to try marker
bar()
unsafe foo() // with "try" this marker is optional (as the above is enough)
baz()
}
foo(execute: {
bar()
baz()
}
foo(execute: {
bar()
foo() // 🛑 that's unsafe call must be marked so
baz()
}
I don't want to get off-topic here, but Swift does not implement either throws or async with a monad transformation behind the scenes. If you'd like to understand how this works, feel free to start a thread in the Development category and @ me.
Given that C/C++ does not have this kind of checking built-in, would they be marked as "unsafe" by default, with an annotation for them to declare themselves otherwise?
That's what Rust does, and in a strict interpretation one would have to assume that all C/C++/ObjC code is potentially unsafe too. That probably isn't practical in reality, though, so we'd have to figure out what the proper balance is.