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

Allow Signals to be used as view indices #124

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions src/Reactive.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ include("core.jl")
include("operators.jl")
include("async.jl")
include("time.jl")
include("arrays.jl")

include("deprecation.jl")

Expand Down
36 changes: 36 additions & 0 deletions src/arrays.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Use Signals as indices for arrays

export freeze

using Base: ViewIndex, tail, to_indexes, to_index, index_shape
typealias RangeCheckedSignal{T,B<:Range} CheckedSignal{T,B}

##### Array overrides #####

Base.to_index(s::CheckedSignal) = to_index(value(s))

function Base.view{T,N}(A::AbstractArray{T,N}, I::Vararg{Union{ViewIndex,RangeCheckedSignal},N})
B = map(bounds, I)
checkbounds(A, B...)
J = to_indexes(I...)
SubArray(A, I, map(length, index_shape(A, J...)))
end

@inline function Base._indices_sub(S::SubArray, pinds, s::RangeCheckedSignal, I...)
i = to_index(s)
Base._indices_sub(S, pinds, i, I...)
end

@inline Base.reindex(V, idxs::Tuple{RangeCheckedSignal, Vararg{Any}}, subidxs::Tuple{Vararg{Any}}) =
Base.reindex(V, (to_index(idxs[1]), tail(idxs)...), subidxs)

"""
freeze(V::SubArray) -> Vnew

Return a new SubArray equivalent to `V` at the time of the `freeze`
call. If `V` has `Signal` indices, they will be converted to static
indices. `Vnew` can be useful if you need to ensure that
signal-indices won't update in the middle of some operation that
`yield`s.
"""
@inline freeze(V::SubArray) = view(parent(V), Base.to_indexes(V.indexes...)...)
103 changes: 90 additions & 13 deletions src/core.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Base: push!, eltype, close
export Signal, push!, value, preserve, unpreserve, close
export AbstractSignal, Signal, CheckedSignal, push!, value, preserve, unpreserve, close

using DataStructures

abstract AbstractSignal{T}

##### Signal #####

const debug_memory = false # Set this to true to debug gc of nodes
Expand All @@ -11,15 +13,15 @@ const nodes = WeakKeyDict()
const io_lock = ReentrantLock()

if !debug_memory
type Signal{T}
type Signal{T} <: AbstractSignal{T}
value::T
parents::Tuple
actions::Vector
alive::Bool
preservers::Dict
end
else
type Signal{T}
type Signal{T} <: AbstractSignal{T}
value::T
parents::Tuple
actions::Vector
Expand Down Expand Up @@ -58,33 +60,36 @@ Signal{T}(::Type{T}, x, parents=()) = Signal{T}(x, parents, Action[], true, Dict
# A signal of types
Signal(t::Type) = Signal(Type, t)

parents(s::Signal) = s.parents
preservers(s::Signal) = s.preservers

# preserve/unpreserve nodes from gc
"""
preserve(signal::Signal)
preserve(signal::AbstractSignal)

prevents `signal` from being garbage collected as long as any of its parents are around. Useful for when you want to do some side effects in a signal.
e.g. `preserve(map(println, x))` - this will continue to print updates to x, until x goes out of scope. `foreach` is a shorthand for `map` with `preserve`.
"""
function preserve(x::Signal)
for p in x.parents
p.preservers[x] = get(p.preservers, x, 0)+1
function preserve(x::AbstractSignal)
for p in parents(x)
preservers(p)[x] = get(preservers(p), x, 0)+1
preserve(p)
end
x
end

"""
unpreserve(signal::Signal)
unpreserve(signal::AbstractSignal)

allow `signal` to be garbage collected. See also `preserve`.
"""
function unpreserve(x::Signal)
for p in x.parents
n = get(p.preservers, x, 0)-1
function unpreserve(x::AbstractSignal)
for p in parents(x)
n = get(preservers(p), x, 0)-1
if n <= 0
delete!(p.preservers, x)
delete!(preservers(p), x)
else
p.preservers[x] = n
preservers(p)[x] = n
end
unpreserve(p)
end
Expand All @@ -99,6 +104,78 @@ value(::Void) = false
eltype{T}(::Signal{T}) = T
eltype{T}(::Type{Signal{T}}) = T

##### CheckedSignal #######

"""
CheckedSignal(val, bounds) -> cs

Create a signal `cs` that throws an error if `push!(cs, val)` does not
satisfy `bounds`. `bounds` can be a range object (in which case the
value must be contained within the range) or a function (in which case
`bounds(val)` must be `true`).

A range-checked `CheckedSignal` may be used as an index in a `view`,
in which case `bounds` must be equal to (or contained within) the
indices for the corresponding dimension of the array.

Example:
```julia
A = rand(5, 6)
col = CheckedSignal(1, indices(A, 2))
V = view(A, :, col) # V == A[:,1]
push!(col, 3)
sleep(0.01) # to let the Reactive.jl message queue run
# Now V == A[:,3]
```
"""
immutable CheckedSignal{T,B} <: AbstractSignal{T}
signal::Signal{T}
bounds::B

function (::Type{CheckedSignal{T,B}}){T,B}(signal::Signal{T}, bounds::B)
checkvalue(bounds, signal)
new{T,B}(signal, bounds)
end
end
CheckedSignal(value, bounds) = CheckedSignal(Signal(value), bounds)
CheckedSignal{T,B}(signal::Signal{T}, bounds::B) = CheckedSignal{T,B}(signal, bounds)

uncheckedsignal(s::Signal) = s
uncheckedsignal(s::CheckedSignal) = s.signal

bounds(s) = s
bounds(s::CheckedSignal) = s.bounds

checkvalue(bounds, signal::Signal) = checkvalue1(bounds, value(signal))
checkvalue(bounds, val) = checkvalue1(bounds, val)
@inline function checkvalue1(bounds::Range, val)
checkindex(Bool, bounds, val) || throw_checkederror(bounds, val)
end
@inline function checkvalue1(f::Function, val)
f(val) || throw_checkederror(f, val)
end
@noinline function throw_checkederror(bounds, val) # @noinline to prevent GC frame alloc.
# should ideally be InexactError or BoundsError, but those aren't flexible enough
throw(ArgumentError("CheckedSignal: $val is not within $bounds"))
end
@noinline function throw_checkederror(f::Function, val)
throw(ArgumentError("CheckedSignal: ($f)($val) was not true"))
end

value(s::CheckedSignal) = value(uncheckedsignal(s))
parents(s::CheckedSignal) = parents(uncheckedsignal(s))
preservers(s::CheckedSignal) = preservers(uncheckedsignal(s))

function Base.push!(s::CheckedSignal, i)
checkvalue(bounds(s), i)
push!(uncheckedsignal(s), i)
end

function Base.map(f, s::Union{Signal,CheckedSignal}...)
map(f, map(uncheckedsignal, s)...)
end


##### Connections #####

function add_action!(f, node, recipient)
Expand Down
1 change: 1 addition & 0 deletions test/REQUIRE
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
FactCheck 0.3.0
OffsetArrays
50 changes: 50 additions & 0 deletions test/arrays.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Reactive, OffsetArrays
using Base.Test

@testset "Indices" begin
@testset "Scalar indexing" begin
A = reshape(1:3*4*2, 3, 4, 2)
s = CheckedSignal(1, 1:2)
V = view(A, :, :, s)
@test V == reshape(1:12, 3, 4)
push!(s, 2)
step()
@test V == reshape(13:24, 3, 4)
@test freeze(V) == reshape(13:24, 3, 4)

s = CheckedSignal(1, 1:3)
@test_throws BoundsError view(A, :, :, s)

V = view(A, s, :, :)
@test V == A[1,:,:]
push!(s, 3)
step()
@test V == A[3,:,:]

V = view(A, :, s, :)
@test V == A[:, 3, :]
end

@testset "Vector indexing" begin
A = reshape(1:3*4*2, 3, 4, 2)
s = CheckedSignal(1:4, 1:4)
V = view(A, :, s, :)
@test V == A
push!(s, 2:3)
step()
@test V == A[:, 2:3, :]
end

@testset "Offset indexing" begin
A = OffsetArray(reshape(1:3*4*2, 3, 4, 2), 1:3, 1:4, 1:2)
s = CheckedSignal(OffsetArray(1:4, 1:4), 1:4)
V = view(A, :, s, :)
@test V == A
push!(s, OffsetArray(2:3, 2:3))
step()
@test indices(V) === (1:3, 2:3, 1:2)
@test V == A[:, OffsetArray(2:3, 2:3), :]
end
end

nothing
30 changes: 30 additions & 0 deletions test/checked.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Reactive, Base.Test

@testset "CheckedSignal" begin
@test_throws ArgumentError CheckedSignal(0, 1:2)
@test_throws ArgumentError CheckedSignal(3, 1:2)
@test_throws ArgumentError CheckedSignal(1, 2:3)
@test value(CheckedSignal(2, 2:3)) == 2
s = @inferred(CheckedSignal(1, 1:2))
ms = map(x->x-100, s)
@test value(ms) == -99
push!(s, 2)
step()
@test value(s) == 2
@test value(ms) == -98
@test_throws ArgumentError push!(s, 3)
@test_throws ArgumentError push!(s, 0)

f(x) = contains(x, "red")
s = CheckedSignal("Fred", f)
@test value(s) == "Fred"
push!(s, "credulous")
step()
@test value(s) == "credulous"
@test_throws ArgumentError push!(s, "read")

preserve(s)
unpreserve(s)
end

nothing
4 changes: 4 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ include("flatten.jl")
include("time.jl")
include("async.jl")
FactCheck.exitstatus()

# Newer Base.Test tests
include("checked.jl")
include("arrays.jl")