diff --git a/src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fs b/src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fs index 07daee5..0043c43 100644 --- a/src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fs +++ b/src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fs @@ -755,7 +755,89 @@ module AdaptiveHashMapImplementation = changes <- HashMap.add i (Set v) changes HashMapDelta.ofHashMap changes - + + /// Reader for batchMap operations. + [] + type BatchMap<'k, 'a, 'b>(input : amap<'k, 'a>, mapping : HashMap<'k,'a> -> HashMap<'k, aval<'b>>) = + inherit AbstractReader>(HashMapDelta.empty) + + let reader = input.GetReader() + do reader.Tag <- "input" + let cacheLock = obj() + let mutable cache: HashMap<'k, aval<'b>> = HashMap.Empty + let mutable targets = MultiSetMap.empty, 'k> + let mutable dirty = HashMap.empty<'k, aval<'b>> + + let consumeDirty() = + lock cacheLock (fun () -> + let d = dirty + dirty <- HashMap.empty + d + ) + + override x.InputChangedObject(t, o) = + #if FABLE_COMPILER + if isNull o.Tag then + let o = unbox> o + for i in MultiSetMap.find o targets do + dirty <- HashMap.add i o dirty + #else + match o with + | :? aval<'b> as o -> + lock cacheLock (fun () -> + for i in MultiSetMap.find o targets do + dirty <- HashMap.add i o dirty + + ) + | _ -> + () + #endif + + override x.Compute t = + let mutable dirty = consumeDirty() + let old = reader.State + let ops = reader.GetChanges t |> HashMapDelta.toHashMap + + let setOps, removeOps = + ((HashMap.empty, HashMap.empty), ops) + ||> HashMap.fold(fun (sets, rems) i op -> + dirty <- HashMap.remove i dirty + cache <- + match HashMap.tryRemove i cache with + | Some (o, remaingCache) -> + let rem, rest = MultiSetMap.remove o i targets + targets <- rest + if rem then o.Outputs.Remove x |> ignore + remaingCache + | None -> cache + match op with + | Set v -> + HashMap.add i v sets, rems + | Remove -> + sets, HashMap.add i Remove rems + ) + + + let mutable changes = HashMap.empty + let setOps = + (setOps, dirty) + ||> HashMap.fold(fun s k _ -> + match HashMap.tryFind k old with + | Some v -> + HashMap.add k v s + | None -> + s + ) + + for i, k in mapping setOps do + cache <- HashMap.add i k cache + let v = k.GetValue t + targets <- MultiSetMap.add k i targets + changes <- HashMap.add i (Set v) changes + + HashMap.union removeOps changes + |> HashMapDelta + /// Reader for chooseA operations. [] type ChooseAReader<'k, 'a, 'b>(input : amap<'k, 'a>, mapping : 'k -> 'a -> aval>) = @@ -1333,6 +1415,30 @@ module AMap = else create (fun () -> MapAReader(map, mapping)) + /// Adaptively applies the given mapping to batches of all changes and e-executes the mapping on dirty outputs + let batchMap (mapping: HashMap<'K, 'T1> -> HashMap<'K, aval<'T2>>) (map: amap<'K, 'T1>) = + if map.IsConstant then + let map = force map |> mapping + if map |> HashMap.forall (fun _ v -> v.IsConstant) then + constant (fun () -> map |> HashMap.map (fun _ v -> AVal.force v)) + else + // TODO better impl possible + create (fun () -> BatchMap(ofHashMap map, id)) + else + create (fun () -> BatchMap(map, mapping)) + + /// + /// Adaptively applies the given mapping to batches of all changes, re-executes the mapping on dirty outputs, including the additional dependencies to be tracked. + /// + let batchMapWithAdditionalDependencies (mapping: HashMap<'K, 'T1> -> HashMap<'K, 'T2 * #seq<#IAdaptiveValue>>) (map: amap<'K, 'T1>) = + let mapping = + mapping + >> HashMap.map(fun _ v -> + AVal.constant v |> AVal.mapWithAdditionalDependencies (id) + ) + batchMap mapping map + + /// Adaptively chooses all elements returned by mapping. let chooseA (mapping: 'K ->'T1 -> aval>) (map: amap<'K, 'T1>) = if map.IsConstant then diff --git a/src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fsi b/src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fsi index 0a1613c..bde4120 100644 --- a/src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fsi +++ b/src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fsi @@ -102,10 +102,17 @@ module AMap = /// Adaptively intersects the two maps. val intersectV : amap<'Key, 'Value1> -> amap<'Key, 'Value2> -> amap<'Key, struct('Value1 * 'Value2)> - /// Adaptively applies the given mapping function to all elements and returns a new amap containing the results. val mapA : mapping: ('K -> 'V -> aval<'T>) -> map: amap<'K, 'V> -> amap<'K, 'T> + /// Adaptively applies the given mapping to batches of all changes and e-executes the mapping on dirty outputs + val batchMap : mapping: (HashMap<'K,'T1> -> HashMap<'K,aval<'T2>>) -> map: amap<'K, 'T1> -> amap<'K, 'T2> + + /// + /// Adaptively applies the given mapping to batches of all changes, re-executes the mapping on dirty outputs, including the additional dependencies to be tracked. + /// + val batchMapWithAdditionalDependencies : mapping: (HashMap<'K, 'T1> -> HashMap<'K, 'T2 * #seq<#IAdaptiveValue>>) -> map: amap<'K, 'T1> -> amap<'K, 'T2> + /// Adaptively chooses all elements returned by mapping. val chooseA : mapping: ('K -> 'V -> aval>) -> map: amap<'K, 'V> -> amap<'K, 'T> diff --git a/src/FSharp.Data.Adaptive/AdaptiveIndexList/AdaptiveIndexList.fs b/src/FSharp.Data.Adaptive/AdaptiveIndexList/AdaptiveIndexList.fs index 71e809e..70b6357 100644 --- a/src/FSharp.Data.Adaptive/AdaptiveIndexList/AdaptiveIndexList.fs +++ b/src/FSharp.Data.Adaptive/AdaptiveIndexList/AdaptiveIndexList.fs @@ -669,7 +669,88 @@ module internal AdaptiveIndexListImplementation = ) changes - + + + /// Reader for mapA operations. + [] + type BatchMapReader<'a, 'b>(input : alist<'a>, mapping : IndexList<'a> -> IndexList>) = + inherit AbstractReader>(IndexListDelta.empty) + + let reader = input.GetReader() + do reader.Tag <- "input" + + let cacheLock = obj() + let mutable cache = IndexList.empty> + let mutable targets = MultiSetMap.empty, Index> + let mutable dirty = IndexList.empty> + + let consumeDirty() = + lock cacheLock (fun () -> + let d = dirty + dirty <- IndexList.empty + d + ) + + override x.InputChangedObject(t, o) = + #if FABLE_COMPILER + if isNull o.Tag then + let o = unbox> o + for i in MultiSetMap.find o targets do + dirty <- IndexList.set i o dirty + #else + match o with + | :? aval<'b> as o -> + lock cacheLock (fun () -> + for i in MultiSetMap.find o targets do + dirty <- IndexList.set i o dirty + ) + | _ -> + () + #endif + + override x.Compute t = + let mutable dirty = consumeDirty() + let old = reader.State + let ops = reader.GetChanges t + let mutable setOps, changes = IndexList.empty, IndexListDelta.empty + + for (i, op) in ops do + dirty <- IndexList.remove i dirty + cache <- + match IndexList.tryRemove i cache with + | Some (o, remainingCache) -> + let rem, rest = MultiSetMap.remove o i targets + targets <- rest + if rem then o.Outputs.Remove x |> ignore + remainingCache + | None -> cache + match op with + | Set v -> + setOps <- IndexList.set i v setOps + | Remove -> + changes <- IndexListDelta.add i Remove changes + + dirty + |> IndexList.iteri(fun i _ -> + match IndexList.tryGet i old with + | Some v -> + setOps <- IndexList.set i v setOps + | None -> + () + ) + + mapping setOps + |> IndexList.iteri(fun i k -> + cache <- IndexList.set i k cache + let v = k.GetValue t + targets <- MultiSetMap.add k i targets + changes <- IndexListDelta.add i (Set v) changes + + ) + + changes + + /// Reader for chooseA operations. [] type ChooseAReader<'a, 'b>(input : alist<'a>, mapping : Index -> 'a -> aval>) = @@ -1461,6 +1542,18 @@ module AList = let mapA (mapping: 'T1 -> aval<'T2>) (list: alist<'T1>) = mapAi (fun _ v -> mapping v) list + /// Adaptively applies the given mapping to batches of all changes and e-executes the mapping on dirty outputs + let batchMap (mapping: IndexList<'T1> -> IndexList>) (list: alist<'T1>) = + if list.IsConstant then + let map = force list |> mapping + if map |> Seq.forall (fun v -> v.IsConstant) then + constant (fun () -> map |> IndexList.map (fun v -> AVal.force v)) + else + // TODO better impl possible + ofReader (fun () -> BatchMapReader(ofIndexList map, id)) + else + ofReader (fun () -> BatchMapReader(list, mapping)) + /// Adaptively chooses all elements returned by mapping. let chooseAi (mapping: Index ->'T1 -> aval>) (list: alist<'T1>) = if list.IsConstant then diff --git a/src/FSharp.Data.Adaptive/AdaptiveIndexList/AdaptiveIndexList.fsi b/src/FSharp.Data.Adaptive/AdaptiveIndexList/AdaptiveIndexList.fsi index b4ee4f2..71646a1 100644 --- a/src/FSharp.Data.Adaptive/AdaptiveIndexList/AdaptiveIndexList.fsi +++ b/src/FSharp.Data.Adaptive/AdaptiveIndexList/AdaptiveIndexList.fsi @@ -96,6 +96,8 @@ module AList = /// Adaptively applies the given mapping function to all elements and returns a new alist containing the results. val mapA : mapping: ('T1 -> aval<'T2>) -> list: alist<'T1> -> alist<'T2> + val batchMap : mapping: (IndexList<'T1> -> IndexList>) -> list: alist<'T1> -> alist<'T2> + /// Adaptively chooses all elements returned by mapping. val chooseAi : mapping: (Index -> 'T1 -> aval>) -> list: alist<'T1> -> alist<'T2> diff --git a/src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fs b/src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fs index 61dceff..6ce56e6 100644 --- a/src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fs +++ b/src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fs @@ -399,6 +399,43 @@ module AVal = inner <- ValueSome (struct (va, vb, vc, res)) res.GetValue token + + /// + /// Calls a mapping function which creates additional dependencies to be tracked. + /// + /// + /// Usecase for this is when a file, such as a .fsproj file changes, it needs to be reloaded in msbuild. + /// Additionally fsproj files have dependencies, such as project.assets.json, that can't be determined until loaded with msbuild + /// but should be reloaded if those dependent files change. + /// + let mapWithAdditionalDependencies (mapping: 'a -> 'b * #seq<#IAdaptiveValue>) (value: aval<'a>) : aval<'b> = + let mutable lastDeps = HashSet.empty + + { new AbstractVal<'b>() with + member x.Compute(token: AdaptiveToken) = + let input = value.GetValue token + + // re-evaluate the mapping based on the (possibly new input) + let result, deps = mapping input + + // compute the change in the additional dependencies and adjust the graph accordingly + let newDeps = HashSet.ofSeq deps + + for op in HashSet.computeDelta lastDeps newDeps do + match op with + | Add(_, d) -> + // the new dependency needs to be evaluated with our token, s.t. we depend on it in the future + d.GetValueUntyped token |> ignore + | Rem(_, d) -> + // we no longer need to depend on the old dependency so we can remove ourselves from its outputs + lock d.Outputs (fun () -> d.Outputs.Remove x) |> ignore + + lastDeps <- newDeps + + result } + :> aval<_> + + /// Aval for custom computations [] type CustomVal<'T>(compute: AdaptiveToken -> 'T) = diff --git a/src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fsi b/src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fsi index 3cd554f..0918ebd 100644 --- a/src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fsi +++ b/src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fsi @@ -108,6 +108,16 @@ module AVal = /// adaptive inputs. val map3 : mapping : ('T1 -> 'T2 -> 'T3 -> 'T4) -> value1 : aval<'T1> -> value2 : aval<'T2> -> value3 : aval<'T3> -> aval<'T4> + /// + /// Calls a mapping function which creates additional dependencies to be tracked. + /// + /// + /// Usecase for this is when a file, such as a .fsproj file changes, it needs to be reloaded in msbuild. + /// Additionally fsproj files have dependencies, such as project.assets.json, that can't be determined until loaded with msbuild + /// but should be reloaded if those dependent files change. + /// + val mapWithAdditionalDependencies : mapping :( 'T1 -> 'T2 * #seq<#IAdaptiveValue>) -> value: aval<'T1> -> aval<'T2> + /// Returns a new adaptive value that adaptively applies the mapping function to the given /// input and adaptively depends on the resulting adaptive value. /// The resulting adaptive value will hold the latest value of the aval<_> returned by mapping. diff --git a/src/Test/FSharp.Data.Adaptive.Tests/AList.fs b/src/Test/FSharp.Data.Adaptive.Tests/AList.fs index b428177..9851f82 100644 --- a/src/Test/FSharp.Data.Adaptive.Tests/AList.fs +++ b/src/Test/FSharp.Data.Adaptive.Tests/AList.fs @@ -8,6 +8,7 @@ open FsUnit open FsCheck.NUnit open FSharp.Data open Generators +open System [ |]); Timeout(60000)>] let ``[AList] reference impl``() ({ lreal = real; lref = ref; lexpression = str; lchanges = changes } : VList) = @@ -1161,4 +1162,95 @@ let ``[AList] mapA inner change``() = ) r.GetChanges AdaptiveToken.Top |> ignore - r.State |> should equal (IndexList.ofSeqIndexed [a, 2; b, -1; c, 6; d, 8; e, 10]) \ No newline at end of file + r.State |> should equal (IndexList.ofSeqIndexed [a, 2; b, -1; c, 6; d, 8; e, 10]) + + +[] +let ``[AList] batchMap``() = + + let file1 = "File1.fs" + let file1Cval = cval 1 + let file1DepCval = cval DateTime.UtcNow + let file2 = "File2.fs" + let file2Cval = cval 2 + let file2DepCval = cval DateTime.UtcNow + let file3 = "File3.fs" + let file3Cval = cval 3 + let file3DepCval = cval DateTime.UtcNow + + let file4 = "File4.fs" + let file4Cval = cval 3 + let file4DepCval = cval DateTime.UtcNow + + let dependencies = Map [file1, file1DepCval; file2, file2DepCval; file3, file3DepCval; file4, file4DepCval] + + let filesCmap = + clist + [ + file1,file1Cval + file2,file2Cval + file3,file3Cval + // file4 added later + ] + let files = + filesCmap + |> AList.mapA(fun (k, v) -> v |> AVal.map(fun v -> k,v)) + + let mutable lastBatch = Unchecked.defaultof<_> + let res = + files + |> AList.batchMap(fun d -> + lastBatch <- d + d + |> IndexList.mapi(fun k (file,_) -> + printfn "k -> %A" k + (dependencies.[file] :> aval<_> + ) + )) + let firstResult = res |> AList.force + lastBatch |> should haveCount 3 + + transact(fun () -> file1Cval.Value <- file1Cval.Value + 1) + + let secondResult = res |> AList.force + lastBatch |> should haveCount 1 + + firstResult.[0] |> should equal secondResult.[0] + firstResult.[1] |> should equal secondResult.[1] + firstResult.[2] |> should equal secondResult.[2] + + transact(fun () -> file1DepCval.Value <- DateTime.UtcNow) + + let thirdResult = res |> AList.force + lastBatch |> should haveCount 1 + + secondResult.[0] |> should not' (equal thirdResult.[0]) + secondResult.[1] |> should equal thirdResult.[1] + secondResult.[2] |> should equal thirdResult.[2] + + transact(fun () -> + file1DepCval.Value <- DateTime.UtcNow + file3Cval.Value <- file3Cval.Value + 1 + ) + + let fourthResult = res |> AList.force + lastBatch |> should haveCount 2 + + thirdResult.[0] |> should not' (equal fourthResult.[0]) + thirdResult.[1] |> should equal fourthResult.[1] + thirdResult.[2] |> should equal fourthResult.[2] + + transact(fun () -> + file1Cval.Value <- file1Cval.Value + 1 + file2DepCval.Value <- DateTime.UtcNow + filesCmap.Add(file4, file4Cval) |> ignore + ) + + let fifthResult = res |> AList.force + lastBatch |> should haveCount 3 + + fourthResult.[0] |> should equal fifthResult.[0] + fourthResult.[1] |> should not' (equal fifthResult.[1]) + fourthResult.[2] |> should equal fifthResult.[2] + fifthResult.[3] |> should equal file4DepCval.Value + diff --git a/src/Test/FSharp.Data.Adaptive.Tests/AMap.fs b/src/Test/FSharp.Data.Adaptive.Tests/AMap.fs index bcf4988..e3a1295 100644 --- a/src/Test/FSharp.Data.Adaptive.Tests/AMap.fs +++ b/src/Test/FSharp.Data.Adaptive.Tests/AMap.fs @@ -8,6 +8,8 @@ open FsUnit open FsCheck.NUnit open FSharp.Data open Generators +open System.IO +open System [ |]); Timeout(60000)>] let ``[AMap] reference impl``() ({ mreal = real; mref = ref; mexpression = str; mchanges = changes } : VMap) = @@ -638,4 +640,191 @@ let ``[AMap] mapA``() = flag.Value <- true ) - res |> AMap.force |> should equal (HashMap.ofList ["A", 2; "B", 4; "C", 6]) \ No newline at end of file + res |> AMap.force |> should equal (HashMap.ofList ["A", 2; "B", 4; "C", 6]) + + + + +[] +let ``[AMap] batchMap``() = + + let file1 = "File1.fs" + let file1Cval = cval 1 + let file1DepCval = cval DateTime.UtcNow + let file2 = "File2.fs" + let file2Cval = cval 2 + let file2DepCval = cval DateTime.UtcNow + let file3 = "File3.fs" + let file3Cval = cval 3 + let file3DepCval = cval DateTime.UtcNow + + let file4 = "File4.fs" + let file4Cval = cval 3 + let file4DepCval = cval DateTime.UtcNow + + let dependencies = Map [file1, file1DepCval; file2, file2DepCval; file3, file3DepCval; file4, file4DepCval] + + let filesCmap = + cmap + [ + file1, file1Cval + file2, file2Cval + file3, file3Cval + // file4 added later + ] + let files = + filesCmap + |> AMap.mapA(fun _ v -> v) + + let mutable lastBatch = Unchecked.defaultof<_> + let res = + files + |> AMap.batchMap(fun d -> + lastBatch <- d + HashMap.ofList [ + for k,v in d do + k, (dependencies.[k] :> aval<_>) + ] + ) + + let firstResult = res |> AMap.force + lastBatch |> should haveCount 3 + + transact(fun () -> file1Cval.Value <- file1Cval.Value + 1) + + let secondResult = res |> AMap.force + lastBatch |> should haveCount 1 + + firstResult.[file1] |> should equal secondResult.[file1] + firstResult.[file2] |> should equal secondResult.[file2] + firstResult.[file3] |> should equal secondResult.[file3] + + transact(fun () -> file1DepCval.Value <- DateTime.UtcNow) + + let thirdResult = res |> AMap.force + lastBatch |> should haveCount 1 + + secondResult.[file1] |> should not' (equal thirdResult.[file1]) + secondResult.[file2] |> should equal thirdResult.[file2] + secondResult.[file3] |> should equal thirdResult.[file3] + + transact(fun () -> + file1DepCval.Value <- DateTime.UtcNow + file2Cval.Value <- file2Cval.Value + 1 + ) + + let fourthResult = res |> AMap.force + lastBatch |> should haveCount 2 + + thirdResult.[file1] |> should not' (equal fourthResult.[file1]) + thirdResult.[file2] |> should equal fourthResult.[file2] + thirdResult.[file3] |> should equal fourthResult.[file3] + + transact(fun () -> + file1Cval.Value <- file1Cval.Value + 1 + file2DepCval.Value <- DateTime.UtcNow + filesCmap.Add(file4, file4Cval) |> ignore + ) + + let fifthResult = res |> AMap.force + lastBatch |> should haveCount 3 + + fourthResult.[file1] |> should equal fifthResult.[file1] + fourthResult.[file2] |> should not' (equal fifthResult.[file2]) + fourthResult.[file3] |> should equal fifthResult.[file3] + fifthResult.[file4] |> should equal file4DepCval.Value + + + +[] +let ``[AMap] batchMapWithAdditionalDependencies``() = + + let file1 = "File1.fs" + let file1Cval = cval 1 + let file1DepCval = cval 1 + let file2 = "File2.fs" + let file2Cval = cval 2 + let file2DepCval = cval 1 + let file3 = "File3.fs" + let file3Cval = cval 3 + let file3DepCval = cval 1 + + let dependencies = Map [file1, file1DepCval; file2, file2DepCval; file3, file3DepCval] + + let files = + [ + file1, file1Cval + file2, file2Cval + file3, file3Cval + ] + |> AMap.ofList + |> AMap.mapA(fun _ v -> v) + + let mutable lastBatch = Unchecked.defaultof<_> + let res = + files + |> AMap.batchMapWithAdditionalDependencies(fun d -> + lastBatch <- d + HashMap.ofList [ + for k,v in d do + k, (Guid.NewGuid(), [dependencies.[k]]) + ] + ) + let firstResult = res |> AMap.force + lastBatch |> should haveCount 3 + + transact(fun () -> file1Cval.Value <- file1Cval.Value + 1) + + let secondResult = res |> AMap.force + lastBatch |> should haveCount 1 + + firstResult.[file1] |> should not' (equal secondResult.[file1]) + firstResult.[file2] |> should equal secondResult.[file2] + firstResult.[file3] |> should equal secondResult.[file3] + + + transact(fun () -> + file1Cval.Value <- file1Cval.Value + 1 + file3Cval.Value <- file3Cval.Value + 1) + + let thirdResult = res |> AMap.force + lastBatch |> should haveCount 2 + + secondResult.[file1] |> should not' (equal thirdResult.[file1]) + secondResult.[file2] |> should equal thirdResult.[file2] + secondResult.[file3] |> should not' (equal thirdResult.[file3]) + + + transact(fun () -> file1DepCval.Value <- file1DepCval.Value + 1) + + let fourthResult = res |> AMap.force + lastBatch |> should haveCount 1 + + thirdResult.[file1] |> should not' (equal fourthResult.[file1]) + thirdResult.[file2] |> should equal fourthResult.[file2] + thirdResult.[file3] |> should equal fourthResult.[file3] + + transact(fun () -> + file1DepCval.Value <- file1DepCval.Value + 1 + file1Cval.Value <- file1Cval.Value + 1) + + let fifthResult = res |> AMap.force + lastBatch |> should haveCount 1 + + fourthResult.[file1] |> should not' (equal fifthResult.[file1]) + fourthResult.[file2] |> should equal fifthResult.[file2] + fourthResult.[file3] |> should equal fifthResult.[file3] + + + transact(fun () -> + file2DepCval.Value <- file2DepCval.Value + 1 + file3Cval.Value <- file3Cval.Value + 1) + + let sixthResult = res |> AMap.force + lastBatch |> should haveCount 2 + + fifthResult.[file1] |> should equal sixthResult.[file1] + fifthResult.[file2] |> should not' (equal sixthResult.[file2]) + fifthResult.[file3] |> should not' (equal sixthResult.[file3]) + + () \ No newline at end of file diff --git a/src/Test/FSharp.Data.Adaptive.Tests/AVal.fs b/src/Test/FSharp.Data.Adaptive.Tests/AVal.fs index dd080a2..2bd49f6 100644 --- a/src/Test/FSharp.Data.Adaptive.Tests/AVal.fs +++ b/src/Test/FSharp.Data.Adaptive.Tests/AVal.fs @@ -275,8 +275,6 @@ let ``[AVal] mapNonAdaptive GC correct``() = transact (fun () -> v.Value <- 100) test |> AVal.force |> should equal 101 - - [] let ``[AVal] multi map non-adaptive and bind``() = let v = AVal.init true @@ -289,3 +287,66 @@ let ``[AVal] multi map non-adaptive and bind``() = transact (fun () -> v.Value <- false) output |> AVal.force |> should equal 1 + + + +[] +let ``[AVal] mapWithAdditionalDependencies``() = + let v = cval 1 + let incrDep (dep : cval<_>) = + dep.Value <- dep.Value + 1 + let mutable dependency1 = Unchecked.defaultof<_> + let newDep1 () = + dependency1 <- cval 2 + dependency1 + let mutable dependency2 = Unchecked.defaultof<_> + let newDep2 () = + dependency2 <- cval 3 + dependency2 + let mutable mappingCalls = 0 + let incrMapping () = + mappingCalls <- mappingCalls + 1 + + let mapping (i : int) = + incrMapping () + // dependencies aren't known until mapping time + i * 2, [newDep1(); newDep2()] + + let output = v |> AVal.mapWithAdditionalDependencies mapping + + output |> AVal.force |> should equal 2 + mappingCalls |> should equal 1 + + transact (fun () -> v.Value <- 2) + output |> AVal.force |> should equal 4 + mappingCalls |> should equal 2 + + transact (fun () -> incrDep dependency1) + output |> AVal.force |> should equal 4 + mappingCalls |> should equal 3 + + + transact (fun () -> incrDep dependency1) + output |> AVal.force |> should equal 4 + mappingCalls |> should equal 4 + + transact (fun () -> v.Value <- 2) + output |> AVal.force |> should equal 4 + mappingCalls |> should equal 4 + + + transact (fun () -> v.Value <- 1) + output |> AVal.force |> should equal 2 + mappingCalls |> should equal 5 + + transact (fun () -> + v.Value <- 1 + incrDep dependency1) + output |> AVal.force |> should equal 2 + mappingCalls |> should equal 6 + + transact (fun () -> + incrDep dependency2 + incrDep dependency1) + output |> AVal.force |> should equal 2 + mappingCalls |> should equal 7 \ No newline at end of file