Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
183 changes: 120 additions & 63 deletions src/Bijections.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,31 @@ import Base:
==,
collect,
delete!,
display,
getindex,
get,
inv,
isempty,
iterate,
length,
setindex!,
show
setindex!

export Bijection,
active_inv,
collect,
display,
domain,
getindex,
image,
inverse,
isempty,
length,
setindex!,
show

struct Bijection{S,T} <: AbstractDict{S,T}
f::Dict{S,T} # map from domain to range
finv::Dict{T,S} # inverse map from range to domain

# standard constructor
function Bijection{S,T}() where {S,T}
F = Dict{S,T}()
G = Dict{T,S}()
new(F, G)
hasvalue

struct Bijection{K,V,F,Finv} <: AbstractDict{K,V}
f::F # map from domain to range
finv::Finv # inverse map from range to domain

function Bijection(f::F, finv::Finv) where {K,V,F<:AbstractDict{K,V},Finv<:AbstractDict{V,K}}
new{K,V,F,Finv}(f, finv)
end

# private, unsafe constructor
function Bijection{S,T}(F::Dict{S,T}, G::Dict{T,S}) where {S,T}
new(F, G)
function Bijection{K,V,F,Finv}(f::F, finv::Finv) where {K,V,F<:AbstractDict{K,V},Finv<:AbstractDict{V,K}}
new{K,V,F,Finv}(f, finv)
end
end

Expand All @@ -67,64 +56,79 @@ function Bijection(x::S, y::T) where {S,T}
return b
end

# F, Finv default to `Dict{K,V}` and `Dict{V,K}` respectively
function Bijection{K,V}(args...; kwargs...) where {K,V}
Bijection{K,V,Dict{K,V},Dict{V,K}}(args...; kwargs...)
end

# Convert an `AbstractDict` to a `Bijection`
function Bijection(dict::AbstractDict{S,T}) where {S,T}
vals = values(dict)
if length(dict) != length(unique(vals))
error("Repeated value found in dict")
end
Bijection(f::F) where {F<:AbstractDict} = Bijection(f, dict_inverse(f))

b = Bijection{S,T}()
for (k, v) in pairs(dict)
b[k] = v
end
return b
function Bijection{K,V,F,Finv}(f::F) where {K,V,F<:AbstractDict{K,V},Finv<:AbstractDict{V,K}}
Bijection{K,V,F,Finv}(f, Finv(Iterators.map(reverse, f)))
end

# Copy constructor
Bijection(b::Bijection) = Bijection(collect(b))
Bijection(b::Bijection) = Bijection(copy(b.f), copy(b.finv))

## Convert a list of pairs to a Bijection
function Bijection(pair_list::Vector{Pair{S,T}}) where {S,T}
p = unique(pair_list) # remove duplicate pairs
n = length(p)
# if not defined, it will just return a copy of `b.f`
Base.copy(b::Bijection) = Bijection(copy(b.f), copy(b.finv))

xs = first.(p)
ys = last.(p)
if length(xs) != length(unique(xs)) || length(ys) != length(unique(ys))
error("Repeated key or value found in pair list")
end
# Convert a list of pairs to a Bijection (already contains empty constructor)
function Bijection{K,V,F,Finv}(pairs::Pair...) where {K,V,F,Finv}
Bijection{K,V,F,Finv}(F(pairs...), Finv(Iterators.map(reverse, pairs)))
end

b = Bijection{S,T}()
for xy in p
x, y = xy
b[x] = y
end
return b
function Bijection{K,V,F,Finv}(pairs::Vector) where {K,V,F,Finv}
Bijection{K,V,F,Finv}(F(pairs...), Finv(Iterators.map(reverse, pairs)))
end

# Decent way to print out a bijection
function show(io::IO, b::Bijection{S,T}) where {S,T}
print(io, "Bijection{$S,$T} (with $(length(b)) pairs)")
# shortcuts for creating a Bijection from a list of pairs
Bijection(pairs::Pair...) = Bijection(Dict(pairs...), Dict(Iterators.map(reverse, pairs)))
Bijection(pairs::Vector) = Bijection(Dict(pairs...), Dict(Iterators.map(reverse, pairs)))

# create a Bijection of same type but no entries
function Base.empty(b::Bijection, ::Type{K}, ::Type{V}) where {K,V}
Bijection(empty(b.f, K, V), empty(b.finv, V, K))
end

# equality checking
==(a::Bijection, b::Bijection) = a.f == b.f

"""
Base.haskey(b::Bijection, x)

Checks if `x` is in the domain of the Bijection `b`.`
"""
Base.haskey(b::Bijection, x) = haskey(b.f, x)

"""
hasvalue(b::Bijection, y)

Checks if `y` is in the image of the Bijection `b`. It is equivalent to checking if the inverse mapping `b.finv` has `y` as a key, so it should as fast as `haskey`.
"""
hasvalue(b::Bijection, y) = haskey(b.finv, y)

# Add a relation to a bijection: b[x] = y
"""
setindex!(b::Bijection, y, x)

For a `Bijection` `b` use the syntax `b[x]=y` to add `(x,y)` to `b`.
"""
function setindex!(b::Bijection, y, x)
if in(x, domain(b)) || in(y, image(b))
error("One of x or y already in this Bijection")
else
b.f[x] = y
b.finv[y] = x
haskey(b.finv, y) && throw(ArgumentError("inserting $x => $y would break bijectiveness"))

# if update of existing key, then remove old value from finv
# TODO test this!!
if haskey(b.f, x)
old_y = b.f[x]
delete!(b.finv, old_y)
end
b

b.f[x] = y
b.finv[y] = x

return b
end

# retreive b[x] where x is in domain
Expand Down Expand Up @@ -152,13 +156,60 @@ end
# the notation b(y) is a shortcut for inverse(b,y)
(b::Bijection)(y) = inverse(b, y)

# WARN this uses internals so it's dangerous! do not make it public
# this is just used for the default case, but in general the method should be extended for new `AbstractDict` types
"""
inverse_dict_type(D::Type{<:AbstractDict})

Returns the type of the inverse dictionary for a given `AbstractDict` type `D`.
This is used internally to create the inverse mapping in a `Bijection`.
"""
function inverse_dict_type(D::Type{<:AbstractDict{K,V}}) where {K,V}
@warn "Using the default `inverse_dict_type` for $D. This may not be optimal for your specific dictionary type."
D.name.wrapper{V,K}
end

inverse_dict_type(::Type{Dict{K,V}}) where {K,V} = Dict{V,K}
inverse_dict_type(::Type{IdDict{K,V}}) where {K,V} = IdDict{V,K}
inverse_dict_type(::Type{Base.ImmutableDict{K,V}}) where {K,V} = Base.ImmutableDict{V,K}
inverse_dict_type(::Type{Base.PersistentDict{K,V}}) where {K,V} = Base.PersistentDict{V,K}

function dict_inverse(d::D) where {D<:AbstractDict}
allunique(values(d)) || throw(ArgumentError("dict is not bijective"))
inverse_dict_type(D)(reverse.(collect(d)))
end

"""
inv(b::Bijection)

Creates a new `Bijection` that is the inverse of `b`.
Subsequence changes to `b` will not affect `inv(b)`.

See also [`active_inv`](@ref).
"""
inv(b::Bijection) = copy(active_inv(b))

"""
active_inv(b::Bijection)

Creates a `Bijection` that is the inverse of `b`.
The original `b` and the new `Bijection` returned are tied together so that changes to one immediately affect the other.
In this way, the two `Bijection`s remain inverses in perpetuity.

See also [`inv`](@ref).
"""
active_inv(b::Bijection) = Bijection(b.finv, b.f)

# Remove a pair (x,y) from a bijection
"""
delete!(b::Bijection, x)

Deletes the ordered pair `(x,b[x])` from `b`.
"""
function delete!(b::Bijection, x)
# replicate `Dict` behavior: if x is not in the domain, do nothing
haskey(b, x) || return b

y = b[x]
delete!(b.f, x)
delete!(b.finv, y)
Expand Down Expand Up @@ -197,6 +248,8 @@ Returns an iterator for the keys for `b`.
"""
domain(b::Bijection) = keys(b)

Base.keys(b::Bijection) = keys(b.f)

# return the image as an array of values
"""
image(b::Bijection)
Expand All @@ -205,11 +258,10 @@ Returns an iterator for the values of `b`.
"""
image(b::Bijection) = values(b)

iterate(b::Bijection{S,T}, s::Int) where {S,T} = iterate(b.f, s)
iterate(b::Bijection{S,T}) where {S,T} = iterate(b.f)
Base.values(b::Bijection) = values(b.f)

# convert a Bijection into a Dict; probably not useful
Base.Dict(b::Bijection) = copy(b.f)
iterate(b::Bijection, s) = iterate(b.f, s)
iterate(b::Bijection) = iterate(b.f)

"""
get(b::Bijection, key, default)
Expand All @@ -220,7 +272,12 @@ function get(b::Bijection, key, default)
get(b.f, key, default)
end

include("inversion.jl")
function Base.sizehint!(b::Bijection, sz)
sizehint!(b.f, sz)
sizehint!(b.finv, sz)
b
end

include("composition.jl")

end # end of module Bijections
27 changes: 0 additions & 27 deletions src/inversion.jl

This file was deleted.

Loading
Loading