Skip to content
This repository has been archived by the owner on Feb 24, 2021. It is now read-only.

Commit

Permalink
Hash typeclass improvements (#194)
Browse files Browse the repository at this point in the history
Co-authored-by: Simon Vergauwen <nomisRev@users.noreply.github.com>
Co-authored-by: Simon Vergauwen <vergauwen.simon@gmail.com>
Co-authored-by: Raúl Raja Martínez <raulraja@gmail.com>
  • Loading branch information
4 people authored Aug 15, 2020
1 parent 5f7e957 commit 8d84f11
Show file tree
Hide file tree
Showing 22 changed files with 439 additions and 258 deletions.
16 changes: 16 additions & 0 deletions arrow-core-data/src/main/kotlin/arrow/core/Hashed.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package arrow.core

import arrow.higherkind
import arrow.typeclasses.Hash

/**
* Wrapper type that caches a values precomputed hash with the value
*
* Provides a fast inequality check with its [Eq] instance and its [Hash] instance will use the cached hash.
*/
@higherkind
data class Hashed<A>(val hash: Int, val value: A) : HashedOf<A> {
companion object {
fun <A> A.fromHash(HA: Hash<A>): Hashed<A> = Hashed(HA.run { this@fromHash.hash() }, this)
}
}
81 changes: 67 additions & 14 deletions arrow-core-data/src/main/kotlin/arrow/typeclasses/Hash.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
package arrow.typeclasses

/**
* A type class used to represent hashing for objects of type [F]
* A type class used to represent non-cryptographic hashing for objects of type [F]
*
* A hash function is a mapping of arbitrary data ([F]) to an output set of fixed size ([Int]). The result, a hash value, is
* most commonly used in collections like HashTable as a lookup value
* most commonly used in collections like HashTable as a lookup value.
*
* > Note: To implement this typeclass for your datatype you have to either implement [hash] or [hashWithSalt] otherwise they will call their
* default implementations recursively and crash.
*
* > Much of the internal structure is based on [hashable](https://hackage.haskell.org/package/hashable-1.3.0.0/)
*
* @see <a href="http://arrow-kt.io/docs/arrow/typeclasses/hash/">Hash documentation</a>
*
*/
interface Hash<in F> : Eq<F> {
interface Hash<in F> {

/**
* Produces a hash for an object of type [F].
*
* @receiver The object to hash
* @returns an int representing the object hash
*
* {: data-executable='true'}
*
* ```kotlin:ank
* ```kotlin:ank:playground
* import arrow.core.extensions.*
*
* fun main(args: Array<String>) {
Expand All @@ -29,7 +33,28 @@ interface Hash<in F> : Eq<F> {
* }
* ```
*/
fun F.hash(): Int
fun F.hash(): Int = hashWithSalt(defaultSalt)

/**
* Produces a hash for an object of type [F] with a given salt.
*
* @receiver The object to hash
* @param salt The salt to apply
* @returns an int representing the objects hash
*
* ```kotlin:ank:playground
* import arrow.core.extensions.*
*
* fun main(args: Array<String>) {
* //sampleStart
* val result = String.hash().run { "MyString".hashWithSalt(10) }
* //sampleEnd
* println(result)
* }
* ```
*
*/
fun F.hashWithSalt(salt: Int): Int = salt.combineHashes(hash())

companion object {

Expand All @@ -39,9 +64,7 @@ interface Hash<in F> : Eq<F> {
* @param hashF function that computes a hash for any object of type [F]
* @returns an instance of [Hash] that is defined by the hashF function
*
* {: data-executable='true'}
*
* ```kotlin:ank
* ```kotlin:ank:playground
* import arrow.typeclasses.Hash
*
* fun main(args: Array<String>) {
Expand All @@ -54,16 +77,36 @@ interface Hash<in F> : Eq<F> {
*/
inline operator fun <F> invoke(crossinline hashF: (F) -> Int): Hash<F> = object : Hash<F> {
override fun F.hash(): Int = hashF(this)
}

override fun F.eqv(b: F): Boolean = this == b
/**
* Construct an instance of [Hash] from a function `(F, Int) -> Int`.
*
* @param hashF function that computes a hash for any object of type [F]
* @returns an instance of [Hash] that is defined by the hashF function
*
* ```kotlin:ank:playground
* import arrow.typeclasses.Hash
* import arrow.typeclasses.combineHashes
*
* fun main(args: Array<String>) {
* //sampleStart
* val result = Hash<String> { s: String, salt: Int -> salt.combineHashes(s.hashCode()) }.run { "MyString".hash() }
* //sampleEnd
* println(result)
* }
* ```
*/
inline operator fun <F> invoke(crossinline hashF: (F, Int) -> Int): Hash<F> = object : Hash<F> {
override fun F.hashWithSalt(salt: Int): Int = hashF(this, salt)
}

/**
* Retrieve an instance of [Hash] where the hash function delegates to kotlin's `Any?.hashCode()` function
*
* @returns an instance of [Hash] that always delegates to kotlin's native hashCode functionality
*
* ```kotlin:ank
* ```kotlin:ank:playground
* import arrow.typeclasses.Hash
*
* fun main(args: Array<String>) {
Expand All @@ -78,8 +121,18 @@ interface Hash<in F> : Eq<F> {

private object HashAny : Hash<Any?> {
override fun Any?.hash(): Int = hashCode()

override fun Any?.eqv(b: Any?): Boolean = this == b
}
}
}

/**
* Combine two hashes
*/
private fun Int.combineHashes(h: Int): Int = (this * 16777619) xor h

/**
* Convenience because a lot of hash instances use this
*/
fun Int.hashWithSalt(salt: Int): Int = salt.combineHashes(this.hashCode())

private const val defaultSalt: Int = 0x087fc72c
31 changes: 31 additions & 0 deletions arrow-core-data/src/test/kotlin/arrow/core/HashedTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package arrow.core

import arrow.core.extensions.eq
import arrow.core.extensions.hash
import arrow.core.extensions.hashed.eq.eq
import arrow.core.extensions.hashed.eqK.eqK
import arrow.core.extensions.hashed.foldable.foldable
import arrow.core.extensions.hashed.hash.hash
import arrow.core.extensions.hashed.show.show
import arrow.core.extensions.show
import arrow.core.test.UnitSpec
import arrow.core.test.generators.genK
import arrow.core.test.generators.hashed
import arrow.core.test.laws.EqKLaws
import arrow.core.test.laws.EqLaws
import arrow.core.test.laws.FoldableLaws
import arrow.core.test.laws.HashLaws
import arrow.core.test.laws.ShowLaws
import io.kotlintest.properties.Gen

class HashedTest : UnitSpec() {
init {
testLaws(
EqLaws.laws(Hashed.eq(Int.eq()), Gen.int().hashed(Int.hash())),
EqKLaws.laws(Hashed.eqK(), Hashed.genK()),
ShowLaws.laws(Hashed.show(Int.show()), Hashed.eq(Int.eq()), Gen.int().hashed(Int.hash())),
FoldableLaws.laws(Hashed.foldable(), Hashed.genK()),
HashLaws.laws(Hashed.hash(), Gen.int().hashed(Int.hash()), Hashed.eq(Int.eq()))
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import arrow.core.Const
import arrow.core.ConstPartialOf
import arrow.core.Either
import arrow.core.EitherPartialOf
import arrow.core.ForHashed
import arrow.core.ForId
import arrow.core.ForListK
import arrow.core.ForNonEmptyList
import arrow.core.ForOption
import arrow.core.ForSequenceK
import arrow.core.ForSetK
import arrow.core.ForTry
import arrow.core.Hashed
import arrow.core.Id
import arrow.core.Ior
import arrow.core.IorPartialOf
Expand All @@ -28,6 +30,7 @@ import arrow.core.Success
import arrow.core.Try
import arrow.core.Validated
import arrow.core.ValidatedPartialOf
import arrow.typeclasses.Hash
import io.kotlintest.properties.Gen

interface GenK<F> {
Expand Down Expand Up @@ -113,3 +116,7 @@ fun Try.Companion.genK() = object : GenK<ForTry> {
Success(it)
}, Gen.throwable().map { Try.Failure(it) })
}

fun Hashed.Companion.genK() = object : GenK<ForHashed> {
override fun <A> genK(gen: Gen<A>): Gen<Kind<ForHashed, A>> = gen.hashed(Hash.any()).map { it as Kind<ForHashed, A> } // This isn't great, but will likely work
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import arrow.core.Either
import arrow.core.Endo
import arrow.core.Eval
import arrow.core.Failure
import arrow.core.Hashed
import arrow.core.Id
import arrow.core.Ior
import arrow.core.Left
Expand Down Expand Up @@ -36,6 +37,7 @@ import arrow.core.k
import arrow.core.toOption
import arrow.typeclasses.Applicative
import arrow.typeclasses.ApplicativeError
import arrow.typeclasses.Hash
import io.kotlintest.properties.Gen
import io.kotlintest.properties.shrinking.DoubleShrinker
import io.kotlintest.properties.shrinking.FloatShrinker
Expand Down Expand Up @@ -191,6 +193,8 @@ fun <A> Gen<A>.eval(): Gen<Eval<A>> =
fun Gen.Companion.char(): Gen<Char> =
Gen.from(('A'..'Z') + ('a'..'z') + ('0'..'9') + "!@#$%%^&*()_-~`,<.?/:;}{][±§".toList())

fun <A> Gen<A>.hashed(HA: Hash<A>): Gen<Hashed<A>> = map { v -> Hashed(HA.run { v.hash() }, v) }

private fun <A, B, R> Gen<A>.alignWith(genB: Gen<B>, transform: (Ior<A, B>) -> R): Gen<R> =
object : Gen<R> {
override fun constants(): Iterable<R> =
Expand Down
15 changes: 11 additions & 4 deletions arrow-core-test/src/main/kotlin/arrow/core/test/laws/HashLaws.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import io.kotlintest.properties.forAll
object HashLaws {

fun <F> laws(HF: Hash<F>, G: Gen<F>, EQ: Eq<F>): List<Law> =
EqLaws.laws(EQ, G) + listOf(
listOf(
Law("Hash Laws: Equality implies equal hash") { equalHash(HF, EQ, G) },
Law("Hash Laws: Multiple calls to hash should result in the same hash") { equalHashM(HF, G) }
Law("Hash Laws: Multiple calls to hash should result in the same hash") { equalHashM(HF, G) },
Law("Hash Laws: Multiple calls to hashWithSalt with the same salt should result in the same hash") { equalHashWithSaltM(HF, G) }
)

private fun <F> equalHash(HF: Hash<F>, EQ: Eq<F>, G: Gen<F>) {
forAll(G, G) { a, b ->
forAll(G, G, Gen.int()) { a, b, salt ->
if (EQ.run { a.eqv(b) })
HF.run { a.hash() == b.hash() }
HF.run { a.hash() == b.hash() } && HF.run { a.hashWithSalt(salt) == b.hashWithSalt(salt) }
else
true
}
Expand All @@ -27,4 +28,10 @@ object HashLaws {
HF.run { a.hash() == a.hash() }
}
}

private fun <F> equalHashWithSaltM(HF: Hash<F>, G: Gen<F>) {
forAll(G, Gen.int()) { a, salt ->
HF.run { a.hashWithSalt(salt) == a.hashWithSalt(salt) }
}
}
}
6 changes: 2 additions & 4 deletions arrow-core/src/main/kotlin/arrow/core/extensions/const.kt
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,8 @@ interface ConstShow<A, T> : Show<Const<A, T>> {
}

@extension
interface ConstHash<A, T> : Hash<Const<A, T>>, ConstEq<A, T> {
interface ConstHash<A, T> : Hash<Const<A, T>> {
fun HA(): Hash<A>

override fun EQ(): Eq<A> = HA()

override fun Const<A, T>.hash(): Int = HA().run { value().hash() }
override fun Const<A, T>.hashWithSalt(salt: Int): Int = HA().run { value().hashWithSalt(salt) }
}
23 changes: 13 additions & 10 deletions arrow-core/src/main/kotlin/arrow/core/extensions/either.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import arrow.typeclasses.Semigroup
import arrow.typeclasses.SemigroupK
import arrow.typeclasses.Show
import arrow.typeclasses.Traverse
import arrow.typeclasses.hashWithSalt
import arrow.core.ap as eitherAp
import arrow.core.combineK as eitherCombineK
import arrow.core.extensions.traverse as eitherTraverse
Expand Down Expand Up @@ -241,20 +242,22 @@ interface EitherShow<L, R> : Show<Either<L, R>> {
}

@extension
interface EitherHash<L, R> : Hash<Either<L, R>>, EitherEq<L, R> {
interface EitherHash<L, R> : Hash<Either<L, R>> {

fun HL(): Hash<L>
fun HR(): Hash<R>

override fun EQL(): Eq<L> = HL()

override fun EQR(): Eq<R> = HR()

override fun Either<L, R>.hash(): Int = fold({
HL().run { it.hash() }
}, {
HR().run { it.hash() }
})
override fun Either<L, R>.hash(): Int =
fold(
{ HL().run { it.hashWithSalt(0) } },
{ HR().run { it.hashWithSalt(1) } }
)

override fun Either<L, R>.hashWithSalt(salt: Int): Int =
fold(
{ l -> HL().run { l.hashWithSalt(salt.hashWithSalt(0)) } },
{ r -> HR().run { r.hashWithSalt(salt.hashWithSalt(1)) } }
)
}

@extension
Expand Down
62 changes: 62 additions & 0 deletions arrow-core/src/main/kotlin/arrow/core/extensions/hashed.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package arrow.core.extensions

import arrow.Kind
import arrow.core.Eval
import arrow.core.ForHashed
import arrow.core.Hashed
import arrow.core.Ordering
import arrow.core.fix
import arrow.extension
import arrow.typeclasses.Eq
import arrow.typeclasses.EqK
import arrow.typeclasses.Foldable
import arrow.typeclasses.Hash
import arrow.typeclasses.Order
import arrow.typeclasses.Show
import arrow.typeclasses.hashWithSalt

@extension
interface HashedEq<A> : Eq<Hashed<A>> {
fun EQA(): Eq<A>
override fun Hashed<A>.eqv(b: Hashed<A>): Boolean =
this.hash == b.hash && EQA().run { value.eqv(b.value) }
}

@extension
interface HashedEqK : EqK<ForHashed> {
override fun <A> Kind<ForHashed, A>.eqK(other: Kind<ForHashed, A>, EQ: Eq<A>): Boolean =
fix().let { (h1, v1) ->
other.fix().let { (h2, v2) ->
h1 == h2 && EQ.run { v1.eqv(v2) }
}
}
}

@extension
interface HashedOrder<A> : Order<Hashed<A>> {
fun ORD(): Order<A>
override fun Hashed<A>.compare(b: Hashed<A>): Ordering = ORD().run { value.compare(b.value) }
override fun Hashed<A>.eqv(b: Hashed<A>): Boolean =
this.hash == b.hash && ORD().run { value.eqv(b.value) }
}

@extension
interface HashedShow<A> : Show<Hashed<A>> {
fun SA(): Show<A>
override fun Hashed<A>.show(): String = "Hashed(${SA().run { value.show() }})"
}

@extension
interface HashedFoldable : Foldable<ForHashed> {
override fun <A, B> Kind<ForHashed, A>.foldLeft(b: B, f: (B, A) -> B): B =
f(b, fix().value)

override fun <A, B> Kind<ForHashed, A>.foldRight(lb: Eval<B>, f: (A, Eval<B>) -> Eval<B>): Eval<B> =
Eval.defer { f(fix().value, lb) }
}

@extension
interface HashedHash<A> : Hash<Hashed<A>> {
override fun Hashed<A>.hash(): Int = hash
override fun Hashed<A>.hashWithSalt(salt: Int): Int = hash.hashWithSalt(salt)
}
Loading

0 comments on commit 8d84f11

Please sign in to comment.