top | item 41574002

(no title)

skrrtww | 1 year 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.

discuss

order

Dagonfly|1 year ago

The 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).

MBCook|1 year ago

Here’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

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|1 year ago

Yeah 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).