Skip to content

Commit

Permalink
Adds mapWithAdditionalDependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
TheAngryByrd committed Mar 19, 2023
1 parent eae4a56 commit 9c82dbc
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 36 deletions.
37 changes: 37 additions & 0 deletions src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,43 @@ module AVal =
inner <- ValueSome (struct (va, vb, vc, res))
res.GetValue token


/// <summary>
/// Calls a mapping function which creates additional dependencies to be tracked.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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
[<Sealed>]
type CustomVal<'T>(compute: AdaptiveToken -> 'T) =
Expand Down
10 changes: 10 additions & 0 deletions src/FSharp.Data.Adaptive/AdaptiveValue/AdaptiveValue.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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>

/// <summary>
/// Calls a mapping function which creates additional dependencies to be tracked.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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.
Expand Down
37 changes: 3 additions & 34 deletions src/Test/FSharp.Data.Adaptive.Tests/AMap.fs
Original file line number Diff line number Diff line change
Expand Up @@ -643,44 +643,13 @@ let ``[AMap] mapA``() =
res |> AMap.force |> should equal (HashMap.ofList ["A", 2; "B", 4; "C", 6])


module AVal =

/// <summary>
/// Calls a mapping function which creates additional dependencies to be tracked.
/// </summary>
let mapWithAdditionalDependenies (mapping: 'a -> 'b * #seq<#IAdaptiveValue>) (value: aval<'a>) : aval<'b> =
let mutable lastDeps = HashSet.empty

{ new AVal.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<_>

module AMap =
let mapWithAdditionalDependenies (mapping: HashMap<'K, 'T1> -> HashMap<'K, 'T2 * #seq<#IAdaptiveValue>>) (map: amap<'K, 'T1>) =
let mapWithAdditionalDependencies (mapping: HashMap<'K, 'T1> -> HashMap<'K, 'T2 * #seq<#IAdaptiveValue>>) (map: amap<'K, 'T1>) =
let mapping =
mapping
>> HashMap.map(fun _ v ->
AVal.constant v |> AVal.mapWithAdditionalDependenies (id)
AVal.constant v |> AVal.mapWithAdditionalDependencies (id)
)
AMap.batchRecalcDirty mapping map

Expand Down Expand Up @@ -712,7 +681,7 @@ let ``[AMap] batchRecalcDirty``() =
let mutable lastBatch = Unchecked.defaultof<_>
let res =
projs
|> AMap.mapWithAdditionalDependenies(fun d ->
|> AMap.mapWithAdditionalDependencies(fun d ->
lastBatch <- d
HashMap.ofList [
for k,v in d do
Expand Down
65 changes: 63 additions & 2 deletions src/Test/FSharp.Data.Adaptive.Tests/AVal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,6 @@ let ``[AVal] mapNonAdaptive GC correct``() =
transact (fun () -> v.Value <- 100)
test |> AVal.force |> should equal 101



[<Test>]
let ``[AVal] multi map non-adaptive and bind``() =
let v = AVal.init true
Expand All @@ -289,3 +287,66 @@ let ``[AVal] multi map non-adaptive and bind``() =

transact (fun () -> v.Value <- false)
output |> AVal.force |> should equal 1



[<Test>]
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

0 comments on commit 9c82dbc

Please sign in to comment.