3 ms·
Since you asked: The provided `drinkable` example I think is pretty bad and it's very surprising to me that this is a headline feature. protocol Drinkable:
by skrrtww 2y ago
Since you asked:
The provided `drinkable` example I think is pretty bad and it's very surprising to me that this is a headline feature.
protocol Drinkable: ~Copyable {
consuming func use()
}
struct Coffee: Drinkable, ~Copyable { /\* ... */ }
struct Water: Drinkable { /* ... \*/ }
func drink(item: consuming some Drinkable & ~Copyable) {
item.use()
}
drink(item: Coffee())
drink(item: Water())
Here we have a drink() function that either accepts something `Copyable` OR non-Copyable (uh, I mean, `~Copyable`) and either consumes it...or doesn't? That seems to me like a fountain for logic errors if the function behaves completely differently with the same signature (which is, in fact, explicitly labeled `consuming`). It seems like it should not just compile if you try to call this with a `Copyable` type like Water, but it does.
The syntax for representing this complex and weird concept of "maybe consuming" being `consuming some Drinkable & ~Copyable` is also just totally gross. Why are we using bitwise operation syntax for some weird and logically incoherent kludge? We cannot apply these & and ~ operators indiscriminately, and they do not mean the same thing that they logically mean in any other context, but this function definition definitely implies that they do.
- MBCook 2y agoHere’s my take. I haven’t used this feature yet so I haven’t dug in too deep. drink() takes a Drinkable. A Drinkables can be non-copyable. Copyable is the default, so it has to mark itself as accepting non-copyables. Coffee is non-copyable. Water doesn’t say which means it’s copyable (the default). You can use a copyable anywhere you’d use a non-copyable since there is no restriction. So since drink can take non-copyables it can also use copyables. I’m guessing the function definition has to list non-copyable otherwise it would only allow copyable drinks since the default is all variables are copyable. “consuming some” means the function takes over the ownership of the non-copyable value. It’s no longer usable in the scope that calls drink(). For the copyable value I’m not sure but since they can be copied I could see that going either way. On syntax: Yeah it’s a bit weird, but there was a big debate about it. They wanted something easy to read and fast to use. NotCopyable<Drinkable> is really clear but typing it over and over would get real old. ~ is not special syntax. My understand is “~Copyable” is the name of the type. You can’t just put ~ in front of anything, like ~Drinkable. But since that’s the syntax used for bitwise negation it’s pretty guessable. & is existing syntax for multiple type assertions. You can see the evolution in this Stack Overflow answer: https://stackoverflow.com/a/24089278 https://stackoverflow.com/a/24089278 Seems to read like C to me. It has to be Drinkable and not Copyable. Like I said I haven’t gotten to use this yet, but it seems like a nice improvement. And I know it’s a step in the path towards making it easier to do safe asynchronous programming, object lifetimes, and other features.
- skrrtww 2y agoYeah after thinking about it a bit more it does make more sense to me. The primary gap I had was, as you allude here: > You can use a copyable anywhere you’d use a non-copyable since there is no restriction. Effectively copyable always conforms to non-copyable, just not the other way around. And the compiler effectively automatically notates literally everything with Copyable, so you need the explicit (& ~Copyable) in the function definition so you're still able to define functions within a ~Copyable protocol that have Copyable semantics. It's very in the weeds, and I still don't like it (I would have preferred NotCopyable since the ~, especially next to the &, directly implies something like bitwise operators), but I guess custom ownership is itself very in the weeds and you will have to think about it hard no matter what approach is taken. I would have expected custom ownership to be fundamentally incompatible with Swift, but clearly it's here; I should probably read about it more so I have a more clear understanding. (I also didn't realize & was extant syntax).
- MBCook 2y agoYeah, I’ll admit it’s hard to get your head around. I had to think about it a couple of times just writing that explanation. It took me a couple of minutes to figure out why it was in the function definition. I guess it had to be but that wasn’t obvious to me at all at first. > And the compiler effectively automatically notates literally everything Right. Just like how all classes in Java extend Object even though you don’t have to literally write it. I believe they’re still working on a more Rust-like borrowing system, but I could be wrong. I know this helped them implement parts of the standard library much better because they could make assumptions that you can’t make with copyable objects. I do get your point about just calling it NotCopyable. I don’t actually know why they settled on the name they did, I didn’t ever try to look that up. Maybe it’s because it’s a special thing that requires compiler machinery and there’s no way for you to make an equivalent?
- Dagonfly 2y agoThe issue is that `~Copyable` is basically an anti-protocol. With a generics definition like `some Drinkable` you are _restricting_ the set of suitable types (from any type to only the ones implementing `Drinkable`) which then _expands_ the available functionality (the method use() becomes available). From the perspective of type `Water`, it's conformance to `Drinkable` expands the functionality. The language designers then get in a pickle if some functionality was assumed to exist for all types (e.g. `Copyable`)! By "conforming" to `~Copyable` you are _removing_ functionality. The type can NOT be copied which was assumed to be universally true before. Now, a generics definition like `some ~Copyable` actually _expands_ the set of suitable types (because Copyable types can be used as if they were non-copyable) and reduces the available functionality. It's the inverse of a regular protocol! It becomes extra confusing if you combine `some Drinkable & ~Copyable` where `Drinkable` and `~Copyable` work in opposite directions. This problem also exists in Rust. `Sized` is a trait (aka protocol) that basically all normal types implement, but you can opt-out by declaring `!Sized`. Then, if you actually want to include all types in your generics, you need to write `?Sized` (read: Maybe-Sized).