Reputation: 51
I am trying to track the usage of values, so I wrap the method generating said value and the inputs(which are also wrapped) into a class I call Dataslot.
I do not know what method and what value I will wrap beforehand, so I tried various ways to write this and thought my code below would work.
But let mutable value = funk unpack
does not seem to result in funk beeing recognized as a function, so the method unpack seems to be the wrong approach, how do i get this to work?
type Dataslot(funk, input:Dataslot[]) as self =
let mutable reffunk= funk
let refinput=input
let unpack= for inpu in refinput do inpu.Value
let mutable value = funk unpack
let uses= ResizeArray<Dataslot>[]
let get1()=
value
let mutable get0=fun()->get1()
let get2()=
value<-reffunk unpack
get0<-fun()->get1()
value
do for inpu in refinput do inpu.Subscribe(self)
member x.setfunk(fu)=
reffunk<-fu
for u in uses do
u.Changed
member x.setinput(index:int, inp:Dataslot)=
refinput.[index].Unsubscribe(self)
refinput.[index]=inp
refinput.[index].Subscribe(self)
for u in uses do
u.Changed
member x.Value
with get()=get0()
member x.Changed=get0<-fun()->get2()
member x.Subscribe(f)=
uses.Add(f) |>ignore
member x.Unsubscribe(f)=
uses.Remove(f) |>ignore
Upvotes: 1
Views: 104
Reputation: 243106
I started answering the question, but I ended up doing a couple of changes to the strucutre of your example, so it is no longer a direct answer - but rather, another approach to solve the problem that I imagine you are trying to solve. Hopefully, this will still help though!
Rather than using a concrete class for Dataslot
, I'm using an interface and I also made the interface generic, so that Dataslot<'T>
represents a value of type 'T
:
type Dataslot<'T> =
abstract Value : 'T
abstract Subscribe : (unit -> unit) -> IDisposable
My subscription mechanims is more similar to how IObservable
works - you give it a function that should be called whenever a value changes and it returns an IDisposable
that you can use to cancel the subscription and stop being notified about changes.
I then defined the following three primitives that you can use to work with data slots (implementation below):
val mutableSlot : initial:'T -> ('T -> unit) * Dataslot<'T>
val immutableSlot : value:'T -> Dataslot<'T>
val ( <*> ) : f:Dataslot<('T -> 'R)> -> a:Dataslot<'T> -> Dataslot<'R>
immutableSlot
creates a data slot that never changes and always returns the initial value.mutableSlot
creates a data slot with an initial value and returns a setter together with data slot. You can use the setter function to change the value in the data slot.<*>
operator takes a data slot containing a function, data slot containing an argument and returns a data slot with the result - the results changes whenever the function or the argument change.It is worth noting that the <*>
operatorn and the immutableSlot
function are a pattern that Haskellers call applicative functor. The nice thing is that thanks to how partial application and currying works, you can now use multiple-argument functions too:
let a = immutableSlot 10
let setB, b = mutableSlot 30
let res = immutableSlot (fun a b -> a + b) <*> a <*> b
let sub = res.Subscribe(fun () ->
printfn "Result changed to: %d" res.Value )
Now you can try triggering the change a few times and then calling Dispose
to unsubscribe from notifications:
setB 32
setB 30
sub.Dispose()
setB 1
The implementation of the three operations is quite similar to some of the code you had originally. The main thing that makes this ugly is keeping track of the handlers that need to be notified when a change happens.
mutableSlot
needs to trigger change event whenever the setter is called:
let mutableSlot initial =
let mutable value = initial
let handlers = ResizeArray<_>()
(fun newValue ->
value <- newValue
for h in handlers do h()),
{ new Dataslot<'T> with
member x.Value = value
member x.Subscribe h =
handlers.Add(h)
{ new IDisposable with
member x.Dispose() = handlers.Remove(h) |> ignore } }
immutableSlot
is easier, because it never chnages:
let immutableSlot value =
{ new Dataslot<'T> with
member x.Value = value
member x.Subscribe _ =
{ new IDisposable with member x.Dispose () = () } }
The <*>
operator is uglier, because it needs to subscribe to notifications on its two arguments. However, to avoid memory leaks, it also needs to unsubscribe when the number of subscriptions registered with it reaches zero (I actually wrote a paper about this memory leak!)
let (<*>) (f:Dataslot<'T -> 'R>) (a:Dataslot<'T>) =
let mutable value = f.Value a.Value
let handlers = ResizeArray<_>()
let update () =
value <- f.Value a.Value
for h in handlers do h()
let mutable fsub = { new IDisposable with member x.Dispose() = () }
let mutable asub = { new IDisposable with member x.Dispose() = () }
{ new Dataslot<'R> with
member x.Value =
if handlers.Count > 0 then value else f.Value a.Value
member x.Subscribe h =
handlers.Add(h)
if handlers.Count = 1 then
fsub <- f.Subscribe(update)
asub <- a.Subscribe(update)
value <- f.Value a.Value
{ new IDisposable with
member x.Dispose() =
handlers.Remove(h) |> ignore
if handlers.Count = 0 then
fsub.Dispose()
asub.Dispose() } }
EDIT: There is quite tricky aspect in the implementation of <*>
which is when does it re-compute its value. If someone subscribes for change notifications, we assume they'll need the value and so we recompute it each time one of the arguments changes (eagerly). When nobody subscribes, we assume they might not access the value so we only recompute lazily when Value
is accessed. We could just subscribe and never un-subscribe (and update always eagerly), but this could lead to memory leaks if you repeatedly subscribe and unsubscribe.
Upvotes: 5