/Equals

Swift utility to help implementing Equatable and Hashable protocols

Primary LanguageSwiftMIT LicenseMIT

Build Status codecov.io CocoaPods Compatible Carthage Compatible Swift Package Manager Compatible Platform support License MIT

Equals

Equals is a Swift µframework to reduce boilerplate code when conforming to Equatable and Hashable protocols.

Requirements

  • Swift 3.0
  • Any Swift 3.0 compatible platform (iOS 8.0+ / Mac OS X 10.10+ / tvOS 9.0+ / watchOS 2.0+ / Linux)

Installation

CocoaPods

Add Equals to your pod file and run pod install:

pod 'Equals', '~> 2.0.0'

Carthage

Add Equals to your Cartfile (package dependency) or Cartfile.private (development dependency):

github "tomquist/Equals" ~> 2.0.0

Swift Package Manager

Add to your Package.swift dependencies:

import PackageDescription

let package = Package(
    // ... your project details
    dependencies: [
        // As a required dependency
        .Package(url: "ssh://git@github.com/tomquist/Equals.git", majorVersion: 2)
    ],
    testDependencies: [
        // As a test dependency
        .Package(url: "ssh://git@github.com/tomquist/Equals.git", majorVersion: 2)
    ]
)

Motivation

Lets start with a simple struct:

struct Person {
    let firstName: String?
    let lastName: String
    let children: [Person]
}

To conform this type to Equatable we first have to declare conformance, e.g. by providing an empty extension and provide an operator overload for ==:

extension Person: Equatable {}

func ==(lhs: Person, rhs: Person) -> Bool {
	return lhs.firstName == rhs.firstName
		&& lhs.lastName == rhs.lastName
		&& lhs.children == rhs.children
}

As you can see, this is a lot of code for such a simple type and each property is listed twice.

Even worse, when conforming to Hashable you additionally have to provide a complex hashValue property:

extension Person: Hashable {
	var hashValue: Int {
        var result = 17
        result = 31 &* result &+ (firstName?.hashValue ?? 0)
        result = 31 &* result &+ lastName.hashValue
        for child in children {
            result = 31 &* result &+ child.hashValue
        }
        return result
    }
}

Demo

Using Equals, this is a lot easier. To conform to Hashable, all you have to do is to conform to the EqualsHashable protocol:

extension Person: EqualsHashable {
    static let hashes: Hashes<Person> = Hashes()
        .append {$0.firstName}
        .append {$0.lastName}
        .append {$0.children}
}

If you don't need hashing capabilities, just conform to the EqualsEquatable protocol:

extension Person: EqualsEquatable {
    static let equals: Equals<Person> = Equals()
        .append {$0.firstName}
        .append {$0.lastName}
        .append {$0.children}
}

That's it! Now you can compare your type, using the == operator, put it into Sets or use it as a Dictionary key.

Advanced usage

Equals currently provides append functions for four types of properties:

  • Equatable/Hashable properties
  • Optional properties of type Equatable/Hashable
  • CollectionType properties where the elements are Equatable/Hashable
  • Any other property, but you have to provide equals and hashValue functions.

Sometimes, you explicitly have to specify which append method to use. E.g. lets change the property children of our Person example above into type Set<Person>. Because Set already conforms to Hashable, we now get a compiler error:

Ambiguous use of 'append(hashable:)'

This is because there are potentially several append methods that could be used in this situation. To avoid this error, we can change our implementation of EqualsHashable into this:

extension Person: EqualsHashable {
    static let hashes: Hashes<Person> = Hashes()
        .append {$0.firstName}
        .append {$0.lastName}
        .append(hashable: {$0.children})
}