diff --git a/src/Reactive.jl b/src/Reactive.jl index 47d9e72..1a7da7c 100644 --- a/src/Reactive.jl +++ b/src/Reactive.jl @@ -6,6 +6,7 @@ include("core.jl") include("operators.jl") include("async.jl") include("time.jl") +include("arrays.jl") include("deprecation.jl") diff --git a/src/arrays.jl b/src/arrays.jl new file mode 100644 index 0000000..be74097 --- /dev/null +++ b/src/arrays.jl @@ -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...)...) diff --git a/src/core.jl b/src/core.jl index 2e48fcd..e80249f 100644 --- a/src/core.jl +++ b/src/core.jl @@ -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 @@ -11,7 +13,7 @@ const nodes = WeakKeyDict() const io_lock = ReentrantLock() if !debug_memory - type Signal{T} + type Signal{T} <: AbstractSignal{T} value::T parents::Tuple actions::Vector @@ -19,7 +21,7 @@ if !debug_memory preservers::Dict end else - type Signal{T} + type Signal{T} <: AbstractSignal{T} value::T parents::Tuple actions::Vector @@ -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 @@ -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) diff --git a/test/REQUIRE b/test/REQUIRE index 8397fb0..af30e9d 100644 --- a/test/REQUIRE +++ b/test/REQUIRE @@ -1 +1,2 @@ FactCheck 0.3.0 +OffsetArrays diff --git a/test/arrays.jl b/test/arrays.jl new file mode 100644 index 0000000..9bc3f40 --- /dev/null +++ b/test/arrays.jl @@ -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 diff --git a/test/checked.jl b/test/checked.jl new file mode 100644 index 0000000..4c6bd06 --- /dev/null +++ b/test/checked.jl @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 75e6fe1..7841eb5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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")