Skip to content
Draft
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
108 changes: 107 additions & 1 deletion src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fs
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,89 @@ module AdaptiveHashMapImplementation =
changes <- HashMap.add i (Set v) changes

HashMapDelta.ofHashMap changes


/// Reader for batchMap operations.
[<Sealed>]
type BatchMap<'k, 'a, 'b>(input : amap<'k, 'a>, mapping : HashMap<'k,'a> -> HashMap<'k, aval<'b>>) =
inherit AbstractReader<HashMapDelta<'k, 'b>>(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<aval<'b>, '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<aval<'b>> 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.
[<Sealed>]
type ChooseAReader<'k, 'a, 'b>(input : amap<'k, 'a>, mapping : 'k -> 'a -> aval<Option<'b>>) =
Expand Down Expand Up @@ -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))

/// <summary>
/// Adaptively applies the given mapping to batches of all changes, re-executes the mapping on dirty outputs, including the additional dependencies to be tracked.
/// </summary>
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<Option<'T2>>) (map: amap<'K, 'T1>) =
if map.IsConstant then
Expand Down
9 changes: 8 additions & 1 deletion src/FSharp.Data.Adaptive/AdaptiveHashMap/AdaptiveHashMap.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -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>

/// <summary>
/// Adaptively applies the given mapping to batches of all changes, re-executes the mapping on dirty outputs, including the additional dependencies to be tracked.
/// </summary>
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<option<'T>>) -> map: amap<'K, 'V> -> amap<'K, 'T>

Expand Down
95 changes: 94 additions & 1 deletion src/FSharp.Data.Adaptive/AdaptiveIndexList/AdaptiveIndexList.fs
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,88 @@ module internal AdaptiveIndexListImplementation =
)

changes



/// Reader for mapA operations.
[<Sealed>]
type BatchMapReader<'a, 'b>(input : alist<'a>, mapping : IndexList<'a> -> IndexList<aval<'b>>) =
inherit AbstractReader<IndexListDelta<'b>>(IndexListDelta.empty)

let reader = input.GetReader()
do reader.Tag <- "input"

let cacheLock = obj()
let mutable cache = IndexList.empty<aval<'b>>
let mutable targets = MultiSetMap.empty<aval<'b>, Index>
let mutable dirty = IndexList.empty<aval<'b>>

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<aval<'b>> 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.
[<Sealed>]
type ChooseAReader<'a, 'b>(input : alist<'a>, mapping : Index -> 'a -> aval<Option<'b>>) =
Expand Down Expand Up @@ -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<aval<'T2>>) (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<Option<'T2>>) (list: alist<'T1>) =
if list.IsConstant then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<aval<'T2>>) -> list: alist<'T1> -> alist<'T2>

/// Adaptively chooses all elements returned by mapping.
val chooseAi : mapping: (Index -> 'T1 -> aval<option<'T2>>) -> list: alist<'T1> -> alist<'T2>

Expand Down
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
Loading