I love this approach, specifically because it builds on top of Hashable without introducing any new protocols. It also feels very familiar to how Comparable leans on default implementations to avoid us implementing all operators. It's a pity we can't use default implementations and have to generate those in the compiler (I guess because we can't force the user to implement one or the other). I also like the names chosen. I would go straight to proposal!
If hash(into:) is part of the Hashable protocol, wouldnât it always need to be synthesized? âOthersâ may wish to call it too, right?
I am a huge fan of this idea too: this form of auto synthesized Hashable conformance coupled with the conditional conformance to Hashable for the various Collections really gives all the flexibility of rolling your own, relying on secure auto generated implementations or do something that is partly custom, but does not require the deeper knowledge of creating good and secure hashes.
I'm not sure I understand the question. Can you provide an example of what you mean?
FWIW, if we made hash(into:) an official part of the Hashable protocol, it would become a regular requirement, just like hashValue is now:
Anyone could manually implement it if synthesized conformance was not available or if it wasn't the right choice for their type. In most cases, I believe hash(into:) would be much easier to implement well than hashValue.
Custom hashing collections outside the standard library could choose to call hash(into:) rather than hashValue to generate their hashes. They'd enjoy the benefits of guaranteed good hashing with no need for any additional postprocessing steps.
The compiler would be able to synthesize a definition of hash(into:) in some, but not all, cases -- just like it is sometimes able to synthesize hashValue now. Synthesis would not become universally available: at least one of hash(into:) or hashValue would still need to be manually implemented for classes, and for types with non-hashable stored properties.
Ah, thanks, I see now.
I mistakenly took Davidâs question and your answer to mean hash(into:) was conditionally synthesized based on whether it was called from the hashValue implementation or not. I couldnât make sense of that, but re-reading the thread I now see that I was misunderstanding the question.
However, I'm not sure that we need all that compiler magic to achieve it. Could we not introduce a refined protocol and use default implementations to provide hashValue in terms of the new protocol? The standard library even has this cool feature that underscored members get hidden, so we could even add a forwards-compatibility interface in to Hashable without anybody noticing.
There would be a new name, but since user-code would have to opt-in to the new functionality (by implementing hash(into:)), I think that's fine. Here's an example:
We could, but I prefer a little bit of compiler magic than introducing a new protocol that users need to know about. Plus, with @lorentey's solution, you can always depend on a Hashable type being able to work with a Hasher.
I don't. People are going to learn about this behaviour and wonder how to achieve the same thing in their protocols. They will likely struggle around with defaults before they uncover this thread deep in a Google search and learn that we have to resort to special-case magic because the language cannot otherwise handle it.
It's not specific to this proposal - I'm worried in general about the volume of compiler magic we seem to be adding. It's sad; personally, it's deeply unsatisfying when you're in a design corner, and you imagine a solution based on something you've seen and used, only to find out that it's literally a unique carve-out in the way the language works; because someone else was in your situation and had more power to change the universe than you do.
It also makes you wonder about the state of the language going forward - if the Swift compiler developers are able carving special holes around common problems, they're not going to feel the limitations that the rest of us do. It's similar to the problem Apple has with WatchKit (WatchKit is a sweet solution that will only ever give us baby apps â Marco.org).
The standard library will likely always have quirks, but I feel like it's just been exploding recently.
I've done a reasonable amount of work in Java and I have to agree. The fact that almost all of the standard library is written in Swift is a breath of fresh air compared to Java, which arbitrarily gates functionality "for the language only". It's really frustrating to write your own numeric type and be forced to write an add method because the + operator can only work with primitive types and String for some reason. It's annoying that arrays have their own initialization syntax that ArrayList can't use.
That being said, IIRC this proposal mentioned something along the lines of the compiler magic going away if we had better reflection, so I think we should wait for that before giving up on it.
This is a fair concern; however, in my opinion, adding an extra protocol would cause far more confusion than the extra compiler magic necessary to synthesize these members. I would object to any solution where users would have to choose between two separate protocols for hashing. (I'd prefer to keep hash(into:) internal rather than doing that.)
Granted, even two alternative requirements within the same protocol will lead to confusion. To resolve this, I propose that we consider hash(into:) to officially replacehashValue, and that we should deprecate the latter in the same release that makes hash(into:) available. I think completely removing hashValue would be far too disruptive a change to consider, so the new compiler magic would exist merely to let us evolve Hashable without breaking source compatibility with existing code.
I agree that it would be far better to do these things outside of the compiler. (And not just because I don't find building Swift ASTs by hand in C++ particularly enjoyable.) Allowing automatic conformance synthesis for protocols outside of the standard library would be a useful feature for a hypothetical future language-level metaprogramming facility. The current Hashable, Equatable, Codable, RawRepresentable synthesizers are nice examples of the kinds of things that should be expressible by a facility like that. However, we don't have that today, and designing it is not in scope for the current topic.
The standard library has always been in a special symbiotic relationship with the Swift compiler: while the stdlib is written in Swift, it is not written in the usual dialect -- beyond gyb, the stdlib has access to tools and privileges that aren't available to normal Swift code, and it uses techniques and idioms that aren't often found in regular code. Optional is the flagship example of this: while it may be defined in the stdlib as a regular enum, it is so much more than just that.
Yes, and in general I'm fine with that, if it's done judiciously. Common types having a small amount of sugar, such as Array or Optional, are fine; the issue arises when too much of the library's features become inaccessible for general programs due to laziness on the part of the language developers or "baby-proofing" so that users don't hurt themselves. Swift is nowhere near that, thankfully, but features like these must be considered carefully. As long as there's a path for them to become legitimate language features rather than hacks to the compiler, I'm generally fine with it.
Sounds like we're in full agreement there. The stdlib is often a testbed for potential future language enhancements -- some of the "magic" is only magic because we don't yet know enough about the problem to produce a well-designed language feature for it. For example, consider how stdlib code has always been magically inlinable/specializable across module boundaries -- and compare that to the universally available @inlinable attribute we're now discussing in SE-0193.
Custom conformance synthesis for user protocols is not currently possible within Swift; however, we do have synthesized protocols in the stdlib, and they provide a nice set of examples for problems such a feature would be expected to solve. The either/or requirement logic of the new Hashable protocol we're discussing adds an interesting new dimension to this design space -- the compiler will probably need a little refactoring to handle such rules well. Conformance synthesis may not be ready to progress beyond magic yet; but we're collecting data and gaining practice.
Note: the dividing line between the standard library and Foundation is precisely whether the feature requires a special relationship with the compiler. If a type doesnât require that relationship, it doesnât belong in the standard library:
How do we decide if something belongs in the standard library or Foundation?
In general, the dividing line should be drawn in overlapping area of what people consider the language and what people consider to be a library feature.
For example, Optional is a type provided by the standard library. However, the compiler understands the concept to provide support for things like optional-chaining syntax. The compiler also has syntax for creating Arrays and Dictionaries.
On the other hand, the compiler has no built-in support for types like URL. URL also ties into more complex functionality like basic networking support. Therefore this type is more appropriate for Foundation.
If at all possible, I would prefer deprecating hashValue in newer versions of Swift so that we can keep the name Hashable for the latest hotness. I don't know how much flexibility @available can give us here, @moiseev probably knows.
Related, maybe slightly OT, but more relevant now that Hashable is getting hotter: does it make sense to keep Hashable a subprotocol of Equatable?
My understanding is that the current Hashable doesn't really mean "something that can produce an hash", but more like "an object that can be used in data structures that use hashes".
That has been quite fine but if someone wants to hop on the new good hashing functionality without having to go through equality, they just can't...
Equality is necessary requirement of hashing collections to detect/resolve conflicts. Do you have some use cases of hashing that doesn't require equality?
My main idea is when you have some type representing a big structure, i.e. a typed representation of a big file. In these cases you usually "trust" the hash to be enough and don't follow with full equality check, given the computational cost of it.
I think @Michael_Ilseman is not saying that values with the same hash are equal, but that a value needs to have a concept of equability to be hashable. Ie, he's not saying that a.hashValue == b.hashValueIMPLIESa == b, he's just saying that a is HashableSHOULD IMPLYa is Equatable.
I would love to replace hashValue with this. I'm still trying to think of alternatives to magic, or if we can limit its scope somehow.
It's interesting because this should be possible, but introduces new @available constraints. Both protocols are entirely compatible with each-other. We would like to:
Add a protocol requirement, with a default implementation up to version X
Add a default implementation of the old requirement, from version X.
I know ABI stability is an important goal for the project, but I'm not sure when we can expect real versioning out of @available.
as I said before, I support the proposal but I'm uncomfortable with all the one-off magic we've been adding (in general).