EP 300 · Back to Basics · Oct 28, 2024 ·Members

Video #300: Back to Basics: Advanced Hashable

smart_display

Loading stream…

Video #300: Back to Basics: Advanced Hashable

Episode: Video #300 Date: Oct 28, 2024 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep300-back-to-basics-advanced-hashable

Episode thumbnail

Description

We zoom out a bit to get a greater appreciation for how Equatable and Hashable are used throughout the greater language and ecosystem, including actors, standard library types, SwiftUI, and more.

Video

Cloudflare Stream video ID: bbda58970fca528c0c8557d00c599daa Local file: video_300_back-to-basics-advanced-hashable.mp4 *(download with --video 300)*

References

Transcript

0:05

So this proves that Swift Data has chosen to use referential identity for the Equatable and Hashable conformance of models, and we think this is the clearest sign that referential equality is typically the correct choice for classes. Even Swift Data models do not use structural equality, and they are classes that primarily represent simple data values held in some external store. So, if they can’t use structural equality, what hope do we have for other classes? Brandon

0:28

There’s another situation where referential equality is the only way to make a reference Equatable , but it has to do with actors instead of classes. If you didn’t know, actors are reference types, but its stored properties are all protected by an isolation domain that requires you to await to access and mutate the properties if you are in a different isolation domain.

0:51

Let’s take a look. Actors

0:58

As an example, consider an actor that just holds onto a boolean for right now: actor Status { var isLoading = false }

1:07

One thing to note is that actors are technically reference types. Swift currently has two flavors of reference types: classes, which are non-isolated reference types, and actors, which have isolation built into them. And we can prove actors are reference types because we can ask them for their object identity and compare them using reference equality: let status = Status() ObjectIdentifier(status) // ObjectIdentifier(0x600003a078a0) status === status // true

1:39

Even though we know the risks of making references conform to Hashable by using the data held inside, suppose we tried to anyway: actor Status: Hashable { … static func == (lhs: Status, rhs: Status) -> Bool { lhs.isLoading == rhs.isLoading } func hash(into hasher: inout Hasher) { hasher.combine(isLoading) } }

2:19

This does not work because the == and hash(into:) functions are synchronous, non-isolated functions, and hence we cannot access the isLoading property: Actor-isolated property ‘isLoading’ can not be referenced from a nonisolated context

2:33

We must be able to await in order to access the isLoading property, but we are not in a context that allows awaiting. We could of course slap on an async to the == function: static func == (lhs: Status, rhs: Status) async -> Bool { await lhs.isLoading == rhs.isLoading }

2:56

…and the specific error about accessing isLoading from a nonisolated context goes away, but now the actor no longer conforms to Equatable : Type ‘Status’ does not conform to protocol ’Equatable’

3:04

And this is because the == requirement from Equatable is not async, and so this == we have defined here cannot satisfy the requirement.

3:18

And all of this of course makes perfect sense. The Equatable and Hashable protocols have no concept of concurrency or isolation domains, nor should they. You should be allowed to call == from anywhere in a code base without having to worry about isolation. And if Swift didn’t complain about this error, then it would allow one to pass an actor into a generic context that only expect a Hashable value: let status = Status() func operation<T: Hashable>(_ value: T) { _ = value.hashValue } This is a completely synchronous function, and so if Swift did allow us to conform our actor to Hashable by accessing the isLoading property, then secretly we would be accessing isLoading in a synchronous context, and thus violating the isolation of the data that the actor is supposed to provide.

4:28

And so if we can’t access any data inside the actor to implement == and hash(into:) , what can we do? Well, we can use the actor’s referential identity. Its identity does not require isolation in order to access, and so we can do the following: actor Status: Hashable { … static func == (lhs: Status, rhs: Status) -> Bool { lhs === rhs } func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } }

4:53

We do have one small error: Actor-isolated instance method ‘hash(into:)’ cannot be used to satisfy nonisolated protocol requirement

5:05

This is just because any method on an actor technically looks asynchronous from the outside since one is required to cross an isolation domain to invoke it.

5:25

However, we are not accessing any isolated data inside the method, and therefore we can mark the method as nonisolated : nonisolated func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } That tells Swift that one can invoke this method from any isolation domain, and hence can be invoked in a synchronous context.

5:41

And this is a completely legitimate thing to do. It is not unsafe to mark methods as nonisolated because Swift still has our back when it comes to concurrency checking. For example, if we accidentally started accessing actor data in a nonisolated context: nonisolated func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) _ = isLoading }

5:59

…we instantly get a compiler error: Actor-isolated property ‘isLoading’ can not be referenced from a nonisolated context This is because we have told Swift that this method is accessible from any isolation domain, but the state in the actor requires isolation to be accessed.

6:27

And so what we are seeing here is that actors make it even more obvious that there really is no sensible conformance of Equatable and Hashable for references types other than to use their referential identity. Actors just make it impossible to even access the state that one would need to provide a structural definition.

6:57

There is only one single situation where structural equality and hashability makes sense for reference types, but it is so narrow that it is hardly ever useful. If your class is final and only holds onto let properties that themselves are value types with well-behaved Hashable conformances, then this is a situation where the structural hash(into:) implementation is OK: final class UserRef2: Hashable { let id: Int let isAdmin: Bool let name: String init(id: Int, isAdmin: Bool = false, name: String) { self.id = id self.isAdmin = isAdmin self.name = name } static func == ( lhs: UserRef2, rhs: UserRef2 ) -> Bool { lhs.id == rhs.id && lhs.isAdmin == rhs.isAdmin && lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(id) hasher.combine(isAdmin) hasher.combine(name) } }

8:00

This is OK to do because this class can never change its data on the inside. So that completely avoids all the problems we ran into through these episodes. But also, this isn’t a super useful class. Really it behaves more like a struct since it is immutable, but with the difference that the memory has been allocated on the heap and is passed around by reference, rather than being allocated on the stack and copied when passed around. And this trick of putting value type data into an immutable class can be handy for performance reasons, But also it’s probably not a technique that should be used often. Instead Swift prefers the copy-on-write approach to performant, large data types, where a struct holds onto internal class storage. That gives you a lot of the benefits with more flexibility.

8:55

So, this kind of class is perfectly OK to define structural equality and hashability, and we can expand this to a few more situations with classes. For example, the properties held in the class can also be classes themselves, but only if they satisfy the same properties we are describing here. And further, we can even allow non- final classes, but each super class must also satisfy all the properties we are describing here. Even with these relaxations, it is still a very strict property to demand of classes, and so not something you would regularly encounter.

9:43

And so at this point you may be wondering: is it ever legitimate to conform a class to Equatable or Hashable ? If you don’t use referential identity to implement those requirements then you can easily break the invariants of reasonable looking code, but then if you do use referential identity it seems like the conformance isn’t very useful.

10:00

Well, we would like everyone to know it is absolutely OK to conform references to these protocols. It just doesn’t happen as often as it does for structs, but it is legitimate to do. And the most common reason to do so is if you have a reference that needs to be used with some API that requires Equatable or Hashable types.

10:24

We even have an example of this in one of the demo projects that we have explored many times over the years on Point-Free. It’s a project called SyncUps , and it’s a recreation of Apple’s Scrumdinger app from a few years ago.

10:39

At the root of the application we use a NavigationStack with a statically typed navigation path that comes from an observable model: NavigationStack(path: $model.path) { … }

10:53

The path property is an array of Destination values: var path: [Destination]

11:01

The Destination type is an enum that holds data for each type of screen that can be pushed onto the stack: enum Destination: Hashable { case detail(SyncUpDetailModel) case meeting(Meeting, syncUp: SyncUp) case record(RecordMeetingModel) }

11:08

And here’s the tricky part. Some of the screens, in particular the detail and record screens, have a lot of behavior in them, and they need to be able to communicate things back to the parent. And so rather than letting those views be little isolated islands of behavior, we integrate their logic into the parent by holding onto their observable models in the parent model.

11:31

In particular, the cases of this Destination enum hold onto the observable models that power those features. And this is where things get tricky. The NavigationStack initializer requires that the data in the path collection be hashable: NavigationStack(path: $model.path) { … } …which means each element must be Hashable . That means the Destination enum must be Hashable : enum Destination: Hashable { case detail(SyncUpDetailModel) case meeting(Meeting, syncUp: SyncUp) case record(RecordMeetingModel) }

12:00

And that in turns means each case of the enum must hold onto Hashable types.

12:05

And now that means that SyncUpDetailModel and RecordMeetingModel must be Hashable , but they are both classes because they are observable: @MainActor @Observable final class SyncUpDetailModel { … }

12:22

So this is a situation where we are forced to make our observable models Hashable because we want them all to be integrated into the parent model, and they need to be used by the NavigationStack at the root of the app.

12:36

And so the only sensible thing to do for this conformance is to use the referential identity of the class to implement the protocol. And that is exactly what we do, however we are able to take a shortcut.

12:44

There is a special protocol we have that provides default implementations of == and hash(into:) based on referential identity, and it’s called HashableObject : extension SyncUpDetailModel: HashableObject {}

13:02

That one line automatically provides the boilerplate of needing to implement the requirements of Hashable using just the object’s identity. This is a handy tool to have if you need to conform your classes to Hashable , and it is already available to you if you are using our Swift Navigation library. Equatable ecosystem: Never and SwiftUI visuals

14:42

We have now gone deep into the topic of Equatable and Hashable . Probably far deeper than anyone expected us to. But as we’ve seen many times, it is a surprisingly tricky topic. There are strict laws that one must adhere to, and when dealing with reference types things get even more complicated. Stephen

14:58

But let’s end this series by looking at a bunch of interesting types in the Swift standard library, as well as the broader Apple ecosystem, and see how they conform to these protocols. Everything from SwiftUI animations and colors, to key paths and asynchronous units of works. These are all concepts that magically have a well-defined notion of equality, but they allows frameworks to unlock some amazing capabilities.

15:19

Let’s take a look.

15:22

The first type we are going to consider is more of an oddity than anything, and that’s the Never type: @frozen public enum Never { }

15:28

It’s an enum with no cases, which means it’s a type that has no values. There is no way to construct values of this type, and sometimes it is known as an uninhabited type. We have discussed this type quite a bit on Point-Free, especially in the early days when we dove deep into “ algebraic data types .”

15:43

We aren’t going to rehash any of that material, and instead we just want to point out that this type does conform to Equatable . We of course can’t see this concretely because it’s not possible to get ahold of a value to check its equality: do { let x: Never x == x }

16:06

But either way, we can see right in the documentation that Never does indeed conform to Equatable . And it doesn’t even matter what is returned from the == function in that conformance since there will never be two Never values to compare.

16:21

There are some other types in the Apple ecosystem that have interesting Equatable conformances. For example, in SwiftUI there is an Animation type: import SwiftUI Animation.linear

16:30

It’s seemingly just a value type: @frozen public struct Animation: Equatable, Sendable { … }

16:38

…but none of the internal data is exposed to us publicly. If we look at the docs we will just see that there are a bunch of static functions and properties for constructing animations.

16:57

And animation is not typically something we typically think of as being an “equatable” thing. Animations are very visual and dynamic things that move and change over time. What does it mean to check if two animations are equal?

17:10

Well, at their core, animations can be defined as mathematical equations that describe their movements, and those equations use coefficients and constants to define them. Those numbers are equatable, and if all of those numbers are equal for two animations, then we can say the animations are equal.

17:25

And so it makes sense to do things like this: Animation.linear == .linear // true Animation.easeOut == .linear // false

17:41

Under the hood there is a numeric description of the linear and easeOut animation, and those numbers differ. We can even see this concretely by printing the animation values: print(Animation.linear) print(Animation.easeOut) This outputs: BezierAnimation(duration: 0.35, curve: (extension in SwiftUI):SwiftUI.UnitCurve.CubicSolver(ax: -2.0, bx: 3.0, cx: 0.0, ay: -2.0, by: 3.0, cy: 0.0)) BezierAnimation(duration: 0.35, curve: (extension in SwiftUI):SwiftUI.UnitCurve.CubicSolver(ax: -0.7399999999999998, bx: 1.7399999999999998, cx: 0.0, ay: -2.0, by: 3.0, cy: 0.0))

18:10

…which shows us that these animations are defined by two very different cubic curves.

18:14

But, we can check if two seemingly different linear animations are equal by supplying a custom duration: Animation.linear == .linear(duration: 0.35) // true Animation.linear == .linear(duration: 0.75) // false

18:38

There are other types that follow this pattern in SwiftUI. For example, the Color type describes a very visual concept, yet does so in a numeric way. So, it makes perfect sense to ask colors if they are equal: Color.white == .white // true Color.black == .white // false Color(white: 1) == .white // true Color(white: 0) == .black // true

19:09

However, with colors there is a secret, extra piece of data that controls how colors are ultimately displayed on screen known as the “color space”. And it is possible to have two colors with the same underlying numeric description, but different color spaces, and such colors will not be equal: Color(.displayP3, white: 1) == Color(.sRGB, white: 1) // false

19:38

But that doesn’t contradict any of the laws of Equatable . This is perfectly fine, it’s just a subtly to be aware of with colors.

19:45

And even paths in SwiftUI are an Equatable concept. Ultimately paths can be defined in terms of a sequence of lines, curves and arcs, all of which are numeric descriptions, and so one can say two paths are equal when their numerical description is equal.

20:01

For example, we can create a triangle path like so: var path1 = Path() path1.addLine(to: .init(x: 10, y: 10)) path1.addLine(to: .init(x: 10, y: 0)) path1.closeSubpath()

20:21

And then another like so: var path2 = Path() path2.addLine(to: .init(x: 10, y: 10)) path2.addLine(to: .init(x: 10, y: 0)) path2.closeSubpath()

20:26

And these two paths will be equal: path1 == path2 // true

20:29

However, we can make one small tweak to the second path by having it draw the edges of the triangle in reverse: var path2 = Path() path2.addLine(to: .init(x: 10, y: 0)) path2.addLine(to: .init(x: 10, y: 10)) path2.closeSubpath()

20:36

…and now these paths are not equal: path1 == path2 // false

20:42

And so it’s really interesting to see types that represent quite complex concepts, such as animations, colors and paths, but ultimately are defined numerically and thus get a very straightforward equatable conformance.

20:52

Other types in the SwiftUI universe are not so lucky. The Text view in SwiftUI is Equatable , and it seems like it should have a quite straightforward implementation. If two Text views have the same underlying string, then surely they must be equal right?

21:13

If we give it a spin on a few simple views, it does seem pretty clear cut: Text("Hello") == Text("Hello") // true Text("Hello") == Text("Hello Blob") // false

21:25

This is what we would expect, and the SwiftUI framework can use this equality check to avoid unnecessary view re-renders by checking if the string being rendered has changed.

21:33

However, string interpolation throws a wrench in this unfortunately. Even something as simple as this breaks what we would hope from equality checking: Text("Hello \("Blob")") == Text("Hello \("Blob")") // false

21:49

The mere act of interpolating a string has made it no longer possible to say these two Text views are equal, even though we are interpolating the exact same string.

21:56

We believe SwiftUI doesn’t try to check the equality of Text views once interpolation is involved is because you can interpolate any kind of value, not just strings. And so the only way to check if two Text values are equal is to render the final version of the string that will be displayed to the user, and that could potentially be expensive and not something SwiftUI wants to do multiple times. Equatable ecosystem: Tasks, key paths, and bindings

23:12

There’s a few more types from the standard library that have interesting Equatable implementations. For example the Task type that represents an unstructured unit of work is somehow Equatable , and even key paths are equatable. And there are also types out there that seem at face value to be Equatable , but for some reason are not.

23:33

Let’s explore these topics.

23:36

Take for example the Task type: @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @frozen public struct Task<Success, Failure>: Sendable where Success: Sendable, Failure: Error { }

23:48

Somehow it is both Equatable and Hashable : @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension Task: Equatable { … } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension Task: Hashable { … }

23:56

Remember that Task represents an unstructured unit of asynchronous work. It immediately starts the async work without suspending the current task or blocking the thread, and it accomplishes this with an escaping closure: @discardableResult public init( priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async -> Success )

24:27

Remember that anytime you see an escaping closure then you know you are exiting the world of structured programming. Escaping closures creating a new, untethered execution context that is not at all related to the current lexical scope we are operating in.

24:43

And so how can such a type possibly be equatable or hashable? There is no underlying data that defines what the task is going to execute, as was the case for animations, and so it can’t be using anything like that. And further, the Equatable and Hashable conformance is completely unconstrained. It doesn’t even depend on the equtability or hashability of the underlying value being returned by the task.

25:21

And so Task ’s conformance to these protocols does the only thing that is reasonable, which is to consider every task created as a completely unique thing. We can see this very clearly by creating some seemingly “equal” tasks that are not actually equal: let task1 = Task {} task1 == task1 // true let task2 = Task {} task1 == task2 // false

25:51

This is happening because the Task type doesn’t take into account anything that is happening inside the task in order to define its equality. Every time you create a task it is given a whole new identity unto itself that is distinct from every other task ever created.

26:02

And we can prove this to ourselves by hoping over to the Swift standard library source code to see how == is defined on Task : @available(SwiftStdlib 5.1, *) extension Task: Equatable { public static func ==(lhs: Self, rhs: Self) -> Bool { UnsafeRawPointer(Builtin.bridgeToRawPointer(lhs._task)) == UnsafeRawPointer(Builtin.bridgeToRawPointer(rhs._task)) } }

26:23

We see that it is just comparing the pointer of some _task value. And we can go to the declaration of that property to see: @available(SwiftStdlib 5.1, *) @frozen public struct Task<Success: Sendable, Failure: Error>: Sendable { @usableFromInline internal let _task: Builtin.NativeObject … } …which shows that under the hood of each task lies an object. And so its the referential identity of that object that determines a task’s equality.

26:57

Key paths are another interesting type in the standard library that has an Equatable and Hashable conformance, but it does actually take into account some defining data in order to determine equality. For example, we can construct two key paths to the count property of String , and they are of course equal: \String.count == \String.count // true

27:23

Whereas the key path to the description property is not equal to the key path to the count property: \String.count == \String.description

27:44

We can even erase the types from the key path so that it is an AnyKeyPath , and the equality operator still works as we expect: (\String.count as AnyKeyPath) == \String.count // true

28:01

The equality operator defined on key paths really does take into account the type the key path is derived from and the property. This means that an array’s count key path is different from String ’s count property: \[Int].count == \String.count // false

28:22

And even an array of integers has a different count key path than an array of strings: \[String].count == \[Int].count // false

28:31

All of these values are also Hashable : (\String.count).hashValue (\String.description).hashValue (\[Int].count).hashValue (\[String].count).hashValue

29:04

And so Swift has really gone above and beyond to provide a sensible Equatable conformance for key paths, even though it seems quite strange. When would one ever need to check two key paths for equality?

29:19

Well, this is actually used a ton. It allows one to hold onto key paths in types while not sacrificing equatability or hashability of the type. For example, a SwiftUI view can hold onto a key path in order to do some work with it, and the view will be able to remain Equatable , allowing SwiftUI to perform optimizations for figuring out when it should or should not re-render the view.

29:41

And the Observation framework uses the Hashable conformance of key paths to keep track of which properties of an observable model were accessed. For example, thanks to the fact that key paths are Hashable , we can have a set of type erased AnyKeyPath s that tracks which properties were accessed: var propertiesAccessed: Set<PartialKeyPath<UserRef>> = [] propertiesAccessed.insert(\.name) propertiesAccessed.insert(\.id) propertiesAccessed // [\.name, \.id]

30:53

We personally have also used the Equatable and Hashable conformance of key paths many times in our various open source libraries. For example, in the Composable Architecture there is an internal type called ScopeID that identifies child stores that have been created. This type must be Hashable because we use it in a dictionary to track the child stores: private var children: [ScopeID<State, Action>: AnyObject] = [:]

31:22

However, the ScopeID is determined by what key paths were used to derive the child store: @_spi(Internals) public struct ScopeID<State, Action>: Hashable { let state: PartialKeyPath<State> let action: PartialCaseKeyPath<Action> }

31:29

…and this is possible since key paths are Hashable .

31:43

It’s really great that the core team had the insight to make sure that key paths were endowed with a sensible Equatable and Hashable to make things like this possible. It at first seems quite strange to think about what it means to check the equality of key paths, but it is incredibly powerful.

31:59

It’s also worth considering a type from the Apple ecosystem, in particular SwiftUI again, that seems like it could be Equatable and Hashable , but it is not. And that’s the Binding type.

32:21

The Binding type is just a struct: @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @frozen @propertyWrapper @dynamicMemberLookup public struct Binding<Value> { … }

32:25

And it’s even a property wrapper that exposes the underlying Value in the binding via a public property: public var wrappedValue: Value { get nonmutating set }

32:36

So, you would think it would be perfectly reasonable to implement equality of two bindings by just looking at the underlying wrapped value: extension Binding: Equatable where Value: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { lhs.wrappedValue == rhs.wrappedValue } }

33:04

This of course produces a warning: Extension declares a conformance of imported type ‘Binding’ to imported protocol ‘Equatable’; this will not behave correctly if the owners of ‘SwiftUICore’ introduce this conformance in the future

33:07

…because it’s not legitimate to conform types you don’t own to protocols you don’t own, but we can ignore this warning for now since we don’t actually intend to conform Binding to Equatable .

33:22

The reason that SwiftUI’s developers chose not to conform Binding to Equatable is because Binding is a reference-y type, even though it is a struct. And the most public clue of this fact can be seen in the interface of the type, in particular the wrappedValue property: public var wrappedValue: Value { get nonmutating set }

33:54

This says that the wrappedValue of a binding can be set, but the act of doing so doesn’t actually mutate the binding.

34:06

For example, we can derive a binding from an observable model like so: @Observable class Model { var count = 0 } @Bindable var model = Model() let count = $model.count // Binding<Int>

34:34

And even though this binding is held onto as a let , we are able to mutate it: count.wrappedValue // 0 count.wrappedValue += 1 count.wrappedValue // 1

34:57

That is because secretly under the hood it is not mutating any state held directly inside the Binding . It is instead mutating the reference that the binding was derived from, in this case the model: model.count // 1

35:21

And this is always the case. All bindings are secretly powered by a reference, and so that makes Binding indistinguishable from a reference. And since we have learned in this series that conforming references to Equatable and Hashable by using their data can be very dangerous, SwiftUI has decided to avoid the entire problem by not conforming the type to Equatable or Hashable .

35:47

Now, SwiftUI could have made Binding equatable in a different way. All bindings are ultimately derived from some base observable object, along with some additional key path transformations for focusing on smaller and smaller parts of the observable model. And all of this data is Hashable , and so could be used to derive a conformance to Hashable if we really wanted.

36:21

And in fact, that’s exactly what we do in our recreation of the Binding type for our Swift Navigation library, which brings the powers of bindings to UIKit and even non-Apple platforms such as Wasm. We can even hop over to the source code for it since it’s open source. @dynamicMemberLookup @propertyWrapper public struct UIBinding<Value>: Sendable { fileprivate let location: any _UIBinding<Value> … }

36:42

It’s called UIBinding , and its public interface looks very similar to SwiftUI’s Binding type. On the inside it is powered by an internal protocol , called _UIBinding , and that protocol requires hashability. protocol _UIBinding<Value>: AnyObject, Hashable, Sendable { associatedtype Value var wrappedValue: Value { get set } }

37:29

And then we have a bunch of conformances to this protocol that represent the various ways one can derive a binding. For example, one can derive a binding to a root reference by using the _UIBindingStrongRoot type: private final class _UIBindingStrongRoot< Root: AnyObject >: _UIBinding, @unchecked Sendable { … }

37:55

And then one can use a key path to derive a binding to a sub-part of that reference using the _UIBindingAppendKeyPath type: private final class _UIBindingAppendKeyPath< Base: _UIBinding, Value >: _UIBinding, @unchecked Sendable { … }

38:10

And this type is Hashable thanks to the fact that key paths are Hashable .

38:24

And so we see that all of the building blocks of UIBinding are Hashable , and so we could make the entire type Hashable . We did not end up doing that, because most people would probably think that a Hashable conformance is going to take into account the underlying wrapped value, but as we’ve seen it cannot.

38:48

But we do expose a type that can be used to identify bindings: public struct UIBindingIdentifier: Hashable { private let location: AnyHashable … }

38:56

…and this is the type we use in the library’s navigation tools to distinguish multiple forms of navigation.

39:21

And so it’s interesting to see how a complex type like Binding could theoretically become Hashable by only considering the object identity of the underlying reference the binding was derived from, as well as any key paths used to transform the binding. Conclusion

39:40

And we have now reached the end of our “Back to basics” series on the Equatable and Hashable protocols in Swift. You may not have thought that it was possible for two people to spend several episodes on two seemingly simple protocols, but I hope we have proven to you that these protocols do have some subtleties that must be taken into consideration. Stephen

39:57

First of all, the protocols are defined in terms of mathematical laws that must be adhered to. If you do not, then you are in for a world of hurt. Simple algorithms will produce bizarre results, or worse, will have very subtle bugs that are nearly impossible to track down. Brandon

40:12

And second, conforming classes to these protocols further complicates these mathematical properties. Because classes are an amalgamation of data and behavior, it really doesn’t make sense to say that their implementations satisfy the laws. This leads you to a situation where you think your conformance is perfectly legitimate, but it will be very easy to write reasonable looking code that produces completely unreasonable results. Stephen

40:39

And so we hope everyone has enjoyed this “Back to basics” series, and we will have a lot more of these in the future. References Hashable Note A type that can be hashed into a Hasher to produce an integer hash value. Documentation for the Swift protocol. https://developer.apple.com/documentation/swift/equatable Equatable Note A type that can be compared for value equality. Documentation for the Swift protocol. https://developer.apple.com/documentation/swift/equatable Equivalence relation Note In mathematics, an equivalence relation is a binary relation that is reflexive, symmetric and transitive. The Wikipedia page defining an “equivalence relation,” a mathematical concept underpinning Swift’s Equatable protocol. https://en.wikipedia.org/wiki/Equivalence_relation Downloads Sample code 0300-back-to-basics-equatable-pt4 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .