Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add == definition of ComposedOptic and IndexLens from Setfield #146

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions src/optics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ using CompositionsBase
using Base: getproperty
using Base

import Base: ==, hash

function make_salt(s64::UInt64)::UInt
# used for faster hashes. See https://github.com/jw3126/Setfield.jl/pull/162
if UInt === UInt64
return s64
else
return UInt32(s64 >> 32)^UInt32(s64 & 0x00000000ffffffff)
end
end

const EXPERIMENTAL = """This function/type is experimental. It can be changed or deleted at any point without warning"""

"""
Expand Down Expand Up @@ -130,6 +141,11 @@ const ComposedOptic{Outer,Inner} = ComposedFunction{Outer,Inner}
outertype(::Type{ComposedOptic{Outer,Inner}}) where {Outer,Inner} = Outer
innertype(::Type{ComposedOptic{Outer,Inner}}) where {Outer,Inner} = Inner

const SALT_COMPOSEDOPTIC = make_salt(0xcf7322dcc2129a31)
Base.hash(l::ComposedOptic, h::UInt) = hash(l.outer, hash(l.inner, SALT_COMPOSEDOPTIC + h))

Base.:(==)(l::ComposedOptic, r::ComposedOptic) = l.outer == r.outer && l.inner == r.inner
sunxd3 marked this conversation as resolved.
Show resolved Hide resolved

# TODO better name
# also better way to organize traits will
# probably only emerge over time
Expand Down Expand Up @@ -416,6 +432,8 @@ Construct a lens for accessing an element of an object at `indices` via `[]`.
"""
IndexLens(indices::Integer...) = IndexLens(indices)

Base.:(==)(l::IndexLens, r::IndexLens) = l.indices == r.indices
sunxd3 marked this conversation as resolved.
Show resolved Hide resolved

Base.@propagate_inbounds function (lens::IndexLens)(obj)
getindex(obj, lens.indices...)
end
Expand Down Expand Up @@ -486,14 +504,5 @@ Broadcast.broadcastable(

Base.:(!)(f::Union{PropertyLens,IndexLens,DynamicIndexLens}) = (!) ∘ f


function make_salt(s64::UInt64)::UInt
# used for faster hashes. See https://github.com/jw3126/Setfield.jl/pull/162
if UInt === UInt64
return s64
else
return UInt32(s64 >> 32)^UInt32(s64 & 0x00000000ffffffff)
end
end
const SALT_INDEXLENS = make_salt(0x8b4fd6f97c6aeed6)
Base.hash(l::IndexLens, h::UInt) = hash(l.indices, SALT_INDEXLENS + h)
65 changes: 65 additions & 0 deletions test/test_core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,71 @@ Base.show(io::IO, ::MIME"text/plain", ::LensIfTextPlain) =
end
end

@testset "equality & hashing" begin
# singletons (identity and property optic) are egal
for (l1, l2) ∈ [
@optic(_) => @optic(_),
@optic(_.a) => @optic(_.a),
]
@test l1 === l2
@test l1 == l2
@test hash(l1) == hash(l2)
end

# composite and index optics are structurally equal
for (l1, l2) ∈ [
@optic(_[1]) => @optic(_[1]),
@optic(_.a[2]) => @optic(_.a[2]),
@optic(_.a.b[3]) => @optic(_.a.b[3]),
@optic(_[1:10]) => @optic(_[1:10]),
@optic(_.a[2:20]) => @optic(_.a[2:20]),
@optic(_.a.b[3:30]) => @optic(_.a.b[3:30]),
]
@test l1 == l2
@test hash(l1) == hash(l2)
end

# inequality
for (l1, l2) ∈ [
@optic(_[1]) => @optic(_[2]),
@optic(_.a[1]) => @optic(_.a[2]),
@optic(_.a[1]) => @optic(_.b[1]),
@optic(_[1:10]) => @optic(_[2:20]),
@optic(_.a[1:10]) => @optic(_.a[2:20]),
@optic(_.a[1:10]) => @optic(_.b[1:10]),
]
@test l1 != l2
end

# equality with non-equal range types (Setfield #165)
for (l1, l2) ∈ [
@optic(_[1:10]) => @optic(_[Base.OneTo(10)]),
@optic(_.a[1:10]) => @optic(_.a[Base.OneTo(10)]),
@optic(_.a.b[1:10]) => @optic(_.a.b[Base.OneTo(10)]),
@optic(_.a[Base.StepRange(1, 1, 5)].b[1:10]) => @optic(_.a[1:5].b[Base.OneTo(10)]),
@optic(_.a.b[1:3]) => @optic(_.a.b[[1, 2, 3]]),
]
@test l1 == l2
@test hash(l1) == hash(l2)
end
Comment on lines +226 to +236
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

== for ComposedFunctions are === by https://github.com/JuliaLang/julia/blob/63e365feb8692b9d7cb5298954c26ab7af268171/base/Base.jl#L208, which means other than the first tuple (tests between IndexLens), all will be false because they are not egal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe create a question/issue for Julia itself?


# Hash property: equality implies equal hashes, or in other terms:
# optics either have equal hashes or are unequal
# Because collisions can occur theoretically (though unlikely), this is a property test,
# not a unit test.
random_optics = (@optic(_.a[rand(Int)]) for _ in 1:1000)
@test all((hash(l2) == hash(l1)) || (l1 != l2)
for (l1, l2) in zip(random_optics, random_optics))

# Optics should hash differently from the underlying tuples, to avoid confusion.
# To account for potential collisions, we check that the property holds with high
# probability.
@test count(hash(@optic(_[i])) != hash((i,)) for i = 1:1000) > 900
sunxd3 marked this conversation as resolved.
Show resolved Hide resolved

# Same for tuples of tuples (√(1000) ≈ 32).
@test count(hash(@optic(_[i][j])) != hash(((i,), (j,))) for i = 1:32, j = 1:32) > 900
sunxd3 marked this conversation as resolved.
Show resolved Hide resolved
sunxd3 marked this conversation as resolved.
Show resolved Hide resolved
end

@testset "type stability" begin
o1 = 2
o22 = 2
Expand Down
Loading