Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT: Improve sub model seq composability #244

Closed

Conversation

TysonMN
Copy link
Member

@TysonMN TysonMN commented Jul 22, 2020

This is a repost as a draft PR of issue #242 since the ability to include specific comments in the corresponding branch is likely useful. The original comment from that issue now follows in full. The branch linked at the start of the second paragraph is now the branch in this PR.


I want the SubModelSeq sample to have more composable code. In particular, I want the counter bindings to be defined with the rest of the counter code and then for those bindings to be "mapped" to higher-level bindings at the App level.

I am trying to achieve this in this branch. Although I am trying to write really good code, I am not suggesting that necessarily merge in any of those commits as is. The primary purpose of this branch is as a proof of concept. I first want to get it working and then consider how to internally structure the code and what the external API should be.

Below, the function Bindings.mapMsgWithModel doesn't exist, but I have almost created it. To get this far, the two important changes are one important change is removing the wrapDispatch feature (which I originally requested, contributed, and later regretted) and changed the type of the binding argument OnCloseRequested from (essentially) 'msg to 'model -> 'msg. I am going to (independently) suggest this second change in a separate issue. (Edited: I did add issue #243 about changing the type of OnCloseRequested, but I realized that it is not important because it doesn't require a change to the public API.)

Tomorrow, I plan to continue trying to create Bindings.mapMsgWithModel. A remaining difficulty is the toMsg binding arguments. I currently don't expect to encounter any other difficulties.

Now to be more specific, I will compare what we have now to what I want. For what we have now, I use the code in PR #241.

Current Code

The counter code is...

type Counter =
  { Count: int
    StepSize: int }

type CounterMsg =
  | Increment
  | Decrement
  | SetStepSize of int
  | Reset

module Counter =

  let init =
    { Count = 0
      StepSize = 1 }
  
  let canReset = (<>) init
  
  let update msg m =
    match msg with
    | Increment -> { m with Count = m.Count + m.StepSize }
    | Decrement -> { m with Count = m.Count - m.StepSize }
    | SetStepSize x -> { m with StepSize = x }
    | Reset -> init

...which lacks bindings while the recursive bindings are

let rec counterBindings () : Binding<Model * Identifiable<Counter>, Msg> list = [
  "CounterIdText" |> Binding.oneWay(fun (_, c) -> c.Id)

  "CounterValue" |> Binding.oneWay(fun (_, c) -> c.Value.Count)
  "Increment" |> Binding.cmd(fun (_, c) -> CounterMsg (c.Id, Increment))
  "Decrement" |> Binding.cmd(fun (_, c) -> CounterMsg (c.Id, Decrement))
  "StepSize" |> Binding.twoWay(
    (fun (_, c) -> float c.Value.StepSize),
    (fun v (_, c) -> CounterMsg (c.Id, SetStepSize (int v))))
  "Reset" |> Binding.cmdIf(
    (fun (_, c) -> CounterMsg (c.Id, Reset)),
    (fun (_, c) -> Counter.canReset c.Value))

  "Remove" |> Binding.cmd(fun (_, c) -> Remove c.Id)

  "AddChild" |> Binding.cmd(fun (_, c) -> AddCounter c.Id)

  "MoveUp" |> Binding.cmdIf(
    (fun (_, c) -> MoveUp c.Id),
    (fun (m, c) -> m |> childrenCountersOfParentOf c.Id |> List.tryHead <> Some c))

  "MoveDown" |> Binding.cmdIf(
    (fun (_, c) -> MoveDown c.Id),
    (fun (m, c) -> m |> childrenCountersOfParentOf c.Id |> List.tryLast <> Some c))

  "GlobalState" |> Binding.oneWay(fun (m, _) -> m.SomeGlobalState)

  "ChildCounters" |> Binding.subModelSeq(
    (fun (m, c) -> m |> childrenCountersOf c.Id),
    (fun ((m, _), childCounter) -> (m, childCounter)),
    (fun (_, c) -> c.Id),
    snd,
    counterBindings)
]

Desired Code

Instead, I want the counter bindings to be defined with the rest of the Counter code like they are in the SingleCounter sample, which would look like...

type Counter =
  { Count: int
    StepSize: int }

type CounterMsg =
  | Increment
  | Decrement
  | SetStepSize of int
  | Reset

module Counter =

  let init =
    { Count = 0
      StepSize = 1 }
  
  let canReset = (<>) init
  
  let update msg m =
    match msg with
    | Increment -> { m with Count = m.Count + m.StepSize }
    | Decrement -> { m with Count = m.Count - m.StepSize }
    | SetStepSize x -> { m with StepSize = x }
    | Reset -> init

  let bindings () : Binding<Counter, CounterMsg> list = [
    "CounterValue" |> Binding.oneWay (fun m -> m.Count)
    "Increment" |> Binding.cmd Increment
    "Decrement" |> Binding.cmd Decrement
    "StepSize" |> Binding.twoWay(
      (fun m -> float m.StepSize),
      int >> SetStepSize)
    "Reset" |> Binding.cmdIf(Reset, canReset)
]

...and then the recursive bindings would be defined something like this

let rec counterBindings () : Binding<Model * Identifiable<Counter>, Msg> list =
  Counter.bindings
  |> Bindings.mapModel (fun (_, c) -> c.Value)
  |> Bindings.mapMsgWithModel (fun (_, c) msg -> CounterMsg (c.Id, msg))
  |> List.append
    [
      "CounterIdText" |> Binding.oneWay(fun (_, c) -> c.Id)
    
      "Remove" |> Binding.cmd(fun (_, c) -> Remove c.Id)
    
      "AddChild" |> Binding.cmd(fun (_, c) -> AddCounter c.Id)
    
      "MoveUp" |> Binding.cmdIf(
        (fun (_, c) -> MoveUp c.Id),
        (fun (m, c) -> m |> childrenCountersOfParentOf c.Id |> List.tryHead <> Some c))
    
      "MoveDown" |> Binding.cmdIf(
        (fun (_, c) -> MoveDown c.Id),
        (fun (m, c) -> m |> childrenCountersOfParentOf c.Id |> List.tryLast <> Some c))
    
      "GlobalState" |> Binding.oneWay(fun (m, _) -> m.SomeGlobalState)
    
      "ChildCounters" |> Binding.subModelSeq(
        (fun (m, c) -> m |> childrenCountersOf c.Id),
        (fun ((m, _), childCounter) -> (m, childCounter)),
        (fun (_, c) -> c.Id),
        snd,
        counterBindings)
    ]

@TysonMN
Copy link
Member Author

TysonMN commented Jul 22, 2020

Got it! :D

See the current state of the branch in this PR. Try the SubModelSeq sample. I also tested it. I think everything still works.

In the SubModelSeq sample, see how the code in CounterTypes is identical to the code in SingleCounter (except the type names are different). This is possible because...

  • BindingData<'model, 'msg> is a contravariant functor in 'model, and I had previously implemented the corresponding map function under the name mapModel.
  • BindingData<'model, 'msg> is a (once again) a covariant functor in 'msg after I removed the wrapDispatch feature in this commit, so I was able to implement the corresponding map function under the name mapMsg.
  • I implemented a function mapMsgWithModel that is stronger than mapMsg in that it accepts a mapping function that not only takes in the source 'msg but also the current 'model. I am not aware of any bugs in my branch, but if one exists, I think it is mostly likely to be in the commits that change OnCloseRequested and ToMsg to accept the current model.
  • I made a bunch of internal types like BindingData<'model, 'msg> public that were previously internal in this commit.. That was just the easiest way to publicly expose mapModel and mapMsgWithModel. If we still want to restrict the user, I think we can make the "constructors" of these types private.

Since mapMsgWithModel is more expressive than mapWithMsg but has a worse name, an idea is to cheat a bit in the names by

  • not exposing the current function called mapMsg and then
  • renaming mapMsgWithModel to mapMsg.

In summary, the two API changes I made were

  • removing the wrapDispatch feature in this commit and
  • making types like BindingData<'model, 'msg> public that were previously internal in this commit.

What do you think @cmeeren?

@TysonMN TysonMN force-pushed the improve_SubModelSeq_Composability branch from a57ecc7 to 71030b9 Compare July 22, 2020 13:06
@TysonMN
Copy link
Member Author

TysonMN commented Jul 22, 2020

@ScottHutchinson, just like PR #241, I think you will be interested in these further improves to the SubModelSeq binding.

@cmeeren
Copy link
Member

cmeeren commented Jul 22, 2020

I'm confused on several fronts here, but first: Currently you're appending a lot of bindings where many overlap the source bindings (Counter.bindings). For example CounterValue, Increment, etc. Is that bug?

@TysonMN
Copy link
Member Author

TysonMN commented Jul 22, 2020

Currently you're appending a lot of bindings where many overlap the source bindings (Counter.bindings). For example CounterValue, Increment, etc. Is that bug?

Yes, that was a mistake. I pushed a fix to remove those duplicate bindings shortly (about 90 minutes) before you posted that comment.

As a side note, I am surprised that these duplicate bindings didn't cause a problem. I am going to investigate that further.

@TysonMN
Copy link
Member Author

TysonMN commented Jul 22, 2020

As a side note, I am surprised that these duplicate bindings didn't cause a problem. I am going to investigate that further.

I forgot what happens when there are bindings with the same name. We log about the duplicate and continue using the first binding.

log "Binding name '%s' is duplicated. Only the first occurance will be used." b.Name

The sample still works because...

Counter.bindings ()
|> Bindings.mapModel (fun (_, c) -> c.Value)
|> Bindings.mapMsgWithModel (fun (_, c) msg -> CounterMsg (c.Id, msg))

...and..

[ "CounterValue" |> Binding.oneWay(fun (_, c) -> c.Value.Count)
  "Increment" |> Binding.cmd(fun (_, c) -> CounterMsg (c.Id, Increment))
  "Decrement" |> Binding.cmd(fun (_, c) -> CounterMsg (c.Id, Decrement))
  "StepSize" |> Binding.twoWay(
    (fun (_, c) -> float c.Value.StepSize),
    (fun v (_, c) -> CounterMsg (c.Id, SetStepSize (int v))))
  "Reset" |> Binding.cmdIf(
    (fun (_, c) -> CounterMsg (c.Id, Reset)),
    (fun (_, c) -> Counter.canReset c.Value)) ]

...are identical, which is the entire goal of this PR.

Here is some output in the logs that I didn't notice before.

Binding name 'CounterValue' is duplicated. Only the first occurance will be used.
Binding name 'Increment' is duplicated. Only the first occurance will be used.
Binding name 'Decrement' is duplicated. Only the first occurance will be used.
Binding name 'StepSize' is duplicated. Only the first occurance will be used.
Binding name 'Reset' is duplicated. Only the first occurance will be used.

Once we have better logging (c.f. #105), it won't be as easy to miss noticing these.

@TysonMN TysonMN force-pushed the improve_SubModelSeq_Composability branch from 71030b9 to 9a19c95 Compare July 22, 2020 17:18
@cmeeren
Copy link
Member

cmeeren commented Jul 22, 2020

I pushed a fix to remove those duplicate bindings shortly (about 90 minutes) before you posted that comment.

Ok, I hadn't started looking at the code, I just read your description.

Continuing, then: I'm in favor of anything that improves Elmish.WPF, but I need to understand this better. It may be the sample, or it may be me missing something here (it's been long since I've delved deeply into Elmish.WPF code), but I can't immediately see why the new API makes things significantly better. I get that mapModel and mapMsgWithModel are general tools that make things more flexible, but they also introduce cognitive overhead. Could you give me some pointers to situations that are significantly improved by these tools? (You don't necessarily have to provide complete code; I'm just trying to understand the core concepts and benefits.)

@TysonMN
Copy link
Member Author

TysonMN commented Jul 22, 2020

I pushed a fix to remove those duplicate bindings shortly (about 90 minutes) before you posted that comment.

Ok, I hadn't started looking at the code, I just read your description.

Good catch. I just removed the "bug" from the code in my initial comment as well.

Could you give me some pointers to situations that are significantly improved by these tools? (You don't necessarily have to provide complete code; I'm just trying to understand the core concepts and benefits.)

Ah, yes. I will give you executable code. This makes me realize that I forgot one thing, which is to "push" the composability into the XAML code. I will improve the SubModelSeq sample further and then report back.

@TysonMN
Copy link
Member Author

TysonMN commented Jul 22, 2020

I just pushed this commit, which extracts the individual counter bindings into their own XAML file.

Now in the SubModelSeq sample, all the individual counter code is completely independent (from the recursive tree portion of the code). As you know @cmeeren, the entry project for these samples is the F# project. If the entry point was the C# project, then they could demonstrate the design-time capabilities of Elmish.WPF. Because all the individual counter code is now completely independent, it can be designed (if it didn't already exist), written, and tested (including via the design-time view) independently of the rest of the application. Before the (binding) code was highly coupled. This change makes the individual counter bindings completely decoupled from the other bindings in the application.

@cmeeren
Copy link
Member

cmeeren commented Jul 22, 2020

Because all the individual counter code is now completely independent, it can be designed (if it didn't already exist), written, and tested (including via the design-time view) independently of the rest of the application. Before the (binding) code was highly coupled. This change makes the individual counter bindings completely decoupled from the other bindings in the application.

That's awesome.

Do you have ideas about how to document the new APIs/functionality? Not just how to use it, but when to use it. Is this only useful for recursive bindings?

If the entry point was the C# project, then they could demonstrate the design-time capabilities of Elmish.WPF.

That would require additional projects for each sample, right? (I know this has been debated before, but I'm unsure if something has changed since then.)


Also, I just noticed a typo, not related to this PR but I'm putting it here before I forget - feel free to fix:

Binding name 'CounterValue' is duplicated. Only the first occurance will be used.

Should be "occurrence".

@TysonMN TysonMN force-pushed the improve_SubModelSeq_Composability branch 2 times, most recently from aad5527 to ed23e43 Compare July 23, 2020 01:15
@TysonMN
Copy link
Member Author

TysonMN commented Jul 23, 2020

Because all the individual counter code is now completely independent, it can be designed (if it didn't already exist), written, and tested (including via the design-time view) independently of the rest of the application. Before the (binding) code was highly coupled. This change makes the individual counter bindings completely decoupled from the other bindings in the application.

That's awesome.

I know!! It makes me so happy :D

Do you have ideas about how to document the new APIs/functionality? Not just how to use it, but when to use it.

On a micro level, mapModel and mapMsg/mapMsgWithModel are great at removing duplicate code. Look at these five bindings again.

"CounterValue" |> Binding.oneWay(fun (_, c) -> c.Value.Count)
"Increment" |> Binding.cmd(fun (_, c) -> CounterMsg (c.Id, Increment))
"Decrement" |> Binding.cmd(fun (_, c) -> CounterMsg (c.Id, Decrement))
"StepSize" |> Binding.twoWay(
  (fun (_, c) -> float c.Value.StepSize),
  (fun v (_, c) -> CounterMsg (c.Id, SetStepSize (int v))))
"Reset" |> Binding.cmdIf(
  (fun (_, c) -> CounterMsg (c.Id, Reset)),
  (fun (_, c) -> Counter.canReset c.Value))

The function arguments getting data from the model and sending it to WPF all do "this": fun (_, c) -> c.Value. The function arguments creating a message all do "this": fun (_, c) msg -> CounterMsg (c.Id, msg). The functions mapModel and mapMsgWithModel are exactly the functions that allow the removal of this duplicate code.

You might recall that I gave another (but admittedly weaker) example in #212 (comment) where I would consider using mapMsg to remove some duplicate code.

On a macro level, the only use case I see is the present one. The items in a SubModelSeq binding each have an ID. When a message "bubbles up" from one of them, it needs to have an ID associated with it so that update is able to route the desired message to the correct item. At the same time, we don't want the bindings specific to that item to have to know about this dependency on an ID. However, that is the current situation in master; all the counter-specific bindings know about this ID.

That counter code doesn't contain any SubModel bindings. Think about how painful it would be to have a SubModel binding in the counter. The code would be so coupled. This approach is completely unreasonable; it doesn't architecturally scale at all.

On the other hand, this one-two punch of mapModel followed by mapMsgWithModel nicely solves this problem. Maybe there is another way to solve this problem, but I am unaware of it.

Is this only useful for recursive bindings?

Not just recursive bindings. This is also helpful in a flat list of submodel bindings.

This reminds me that our SubModelSeq sample is a bit intense. I think it would be helpful if there were a sample that demonstrated the SubModelSeq binding using a flat list.

On a related note, #236 is about the idea of creating a binding specifically for trees (aka recursive bindings). @ScottHutchinson has identified that the existing code is very inefficient. If we created a binding for trees, then we might be able to simplify the current SubModelSeq binding.

If the entry point was the C# project, then they could demonstrate the design-time capabilities of Elmish.WPF.

That would require additional projects for each sample, right? (I know this has been debated before, but I'm unsure if something has changed since then.)

No extra projects needed. One C# project and one F# project is sufficient. The C# project can be the executable project. It instantiaties MainWindow and then calls the existing entry point in the F# project. See this branch where I just made the this change for the SingleCounter sample. I could make this branch into its own PR and make the same change for all the other samples.

I think this would be a good change. I think it would be better to show people with executable code how design-time support works than only describe it in the README. (However, those words in the README are excellent. I prefer to leave them just as they are.)

Also, I just noticed a typo, not related to this PR but I'm putting it here before I forget - feel free to fix:

Binding name 'CounterValue' is duplicated. Only the first occurance will be used.

Should be "occurrence".

Great catch. I pushed a fix to master.

@cmeeren
Copy link
Member

cmeeren commented Jul 23, 2020

For simplicity, I want to split off any changes that can be merged into master now.

Good idea. I'll take a look at this PR when you mark it as "Ready for review". Let me know in a comment before that if there's something in particular you want me to look at.

On a micro level, ...
On a macro level, ...

Thanks for the helpful explanations!

This reminds me that our SubModelSeq sample is a bit intense. I think it would be helpful if there were a sample that demonstrated the SubModelSeq binding using a flat list.

That would be great. If you want to have a go at that, knock yourself out.

No extra projects needed. One C# project and one F# project is sufficient. The C# project can be the executable project. It instantiaties MainWindow and then calls the existing entry point in the F# project.

I see, so more or less what I suggested in #93 except that the F# projects are merged?

See this branch where I just made the this change for the SingleCounter sample. I could make this branch into its own PR and make the same change for all the other samples.

I think that's a great idea. (Why haven't we done that already?) Feel free to go ahead with this.

@TysonMN
Copy link
Member Author

TysonMN commented Jul 23, 2020

No extra projects needed. One C# project and one F# project is sufficient. The C# project can be the executable project. It instantiaties MainWindow and then calls the existing entry point in the F# project.

I see, so more or less what I suggested in #93 except that the F# projects are merged?

Yep.

See this branch where I just made the this change for the SingleCounter sample. I could make this branch into its own PR and make the same change for all the other samples.

I think that's a great idea. (Why haven't we done that already?) Feel free to go ahead with this.

I created issue #246 for this work.

@TysonMN
Copy link
Member Author

TysonMN commented Jul 23, 2020

This reminds me that our SubModelSeq sample is a bit intense. I think it would be helpful if there were a sample that demonstrated the SubModelSeq binding using a flat list.

That would be great. If you want to have a go at that, knock yourself out.

I created issue #247 for this work.

@TysonMN
Copy link
Member Author

TysonMN commented Jul 23, 2020

I'll take a look at this PR when you mark it as "Ready for review". Let me know in a comment before that if there's something in particular you want me to look at.

Yes, there is one thing. Please consider the implications of removing the wrapDispatch feature, which I did in this commit. This is a breaking change with no migration path.

I originally requested, contributed, and later regretted this wrapDispatch feature when I realized it prevents exactly the kind of feature I added in this PR (specifically mapMsg). I think the the wrapDispatch feature (as implemented) and mapMsg feature added in this PR are mutually exclusive, and I think the mapMsg feature is more useful.

Even though I originally requested the wrapDispatch feature, I am not currently using it in my project at work. Furthermore, I admit that I was lazy in implementing this wrapDispatch feature. The feature I actually desired and still think would be good is the ability to adjust the stream of messages leaving a binding via the high-level concepts of throttling, debouncing, and limiting. I don't know of any useful way to wrap dispatch except by using one of those concepts. I was lazy in that I decided to only implement this less useful, more raw, but also more expressive wrapDispatch feature than directly implementing the concepts of throttling, debouncing, and limiting.

So although there might not be a migration path in theory, maybe a migration path in practice is to add support for the higher-level concepts of throttling, debouncing, and limiting. I say "maybe" because I will need to verify (by implementing one of them) that these features do not prevent BindingData<'model, 'msg> from being a covariant functor in 'msg.

@cmeeren
Copy link
Member

cmeeren commented Jul 23, 2020

If we need to make that breaking change with no migration path to identical functionality, we should announce that in an issue and request feedback, and leave that up for a while. And before that we should look at the existing discussions of the feature and any comments we have received there.

Is it really impossible to wrap dispatch when we have mapMsg? If we can implement throttling/debouncing, wouldn't it also be possible to implement a generic wrapDispatch? (Note, I haven't looked at the code, and cocariance/contravariance currently just makes my head hurt).

@TysonMN TysonMN force-pushed the improve_SubModelSeq_Composability branch from ed23e43 to 27a1294 Compare August 5, 2020 16:48
@TysonMN
Copy link
Member Author

TysonMN commented Aug 5, 2020

PR #249 is now complete, which added design-time view models to our samples. I rebased the branch in this PR onto master.

In #244 (comment), I said (without that cross out, which I added in the quote):

If the entry point was the C# project, then they could demonstrate the design-time capabilities of Elmish.WPF. Because all the individual counter code is now completely independent, it can be designed (if it didn't already exist), written, and tested (including via the design-time view) independently of the rest of the application. Before the (binding) code was highly coupled. This change makes the individual counter bindings completely decoupled from the other bindings in the application.

Now the C# project is the entry point, and we do have design-time view models in use. After rebasing I added one new commit, which is to hook up the view model for Counter.xaml.

@TysonMN
Copy link
Member Author

TysonMN commented Aug 5, 2020

Is it really impossible to wrap dispatch [with an arbitrary wrapDispatch function] when we have mapMsg?

Yes, it really is impossible. Here is my argument.

BindingData<'model, 'msg> is a DU. One of its cases is TwoWayData of TwoWayData<'model, 'msg, obj>, where

type internal TwoWayData<'model, 'msg, 'a> = {
  Get: 'model -> 'a
  Set: 'a -> 'model -> 'msg
  WrapDispatch: Dispatch<'msg> -> Dispatch<'msg>
}

The type of WrapDispatch is Dispatch<'msg> -> Dispatch<'msg>. This type is invariant in 'msg. Therefore TwoWayData<'model, 'msg, obj> and BindingData<'model, 'msg> is also invariant in 'msg.

On the other hand, the function mapMsg is the function that the Haskell wiki documentation for functor calls fmap. Although "covariant" doesn't appear on that page, it is about about a covariant functor. Most functors and monads are covariant, so this adjective is often omitted. For extra proof that this page really is about a covariant functor, see and contrast the Haskell documentation for Data.Functor and Data.Functor.Covariant.

Wikipedia has an article called Covariance and contravariance (computer science). Overall, I don't like this article very much. I find it too verbose. However, the initial section of the Formal definition subsection clearly and succinctly defines covariant, contravariant, bivariant, variant, and invariant. Another downside of this Wikipedia is that it definitions use the concept of subtyping, which is a form of polymorphism, which is very common in OOP. In contrast, my experience (mostly with F#) is that prefers FP tries to avoid polymorphism (although the Java dependency ZIO claims to both be FP and polymorphic as I read here).

Anyway, the takeaway (as the names suggest) is that it is impossible to be both covariant and invariant at the same time.

Edited to add an example

Here is an example to help make this more familiar. A generic type F<'a> is a covariant functor in 'a if there exists a function map satisfying the following two conditions

  1. the type of map is ('a -> 'b) -> F<'a> -> F<'b> and
  2. map satisfies the functor laws.

However, ignore the functor laws for a moment and suppose F<'a> = ('a -> ()) * (() -> 'a). Then given

  • an instance of type ('a -> ()) * (() -> 'a) and
  • a function f of type 'a -> 'b,

try to create an instance of type ('b -> ()) * (() -> 'b). You can't because ('a -> ()) * (() -> 'a) is invariant* in 'a.

(*I might be slightly lying here. Suppose F<'a> = 'a -> 'a, which I think is also invariant in 'a. Then I can satisfy condition 1 above via fun _ _ = id. However, it should be no surprise that such an implementation for map does not satisfy the functor laws. Interestingly, it satisfies the composition law but not the identity law).

@TysonMN
Copy link
Member Author

TysonMN commented Aug 5, 2020

If we can implement throttling/debouncing, wouldn't it also be possible to implement a generic wrapDispatch?

I don't think so. My idea here is to name the specific types of dispatch wrapping. Something like

type DispatchWrapper =
  | Limit of LimitParams
  | Throttle of ThrottleParams
  | Debounce of DebounceParams

I would change our current

  • wrapDispatch of type Dispatch<'msg> -> Dispatch<'msg> to
  • dispatchWrappers of type DispatchWrapper seq

...and then change...

let dispatch' = wrapDispatch dispatch

...to something like...

let dispatch' =
  dispatchWrappers
  |> Seq.map toDispatchWrappingFunction
  |> Seq.fold (>>) id
  <| dispatch

This (avoids the previous impossibility and) works because the concepts of throttling, debouncing, and limiting a data stream do not depend on the type of the data in the steam. Specifically, mapMsg (and mapMsgWithModel) would leave dispatchWrappers unchanged.

@cmeeren
Copy link
Member

cmeeren commented Aug 5, 2020

However, ignore the functor laws for a moment and suppose F<'a> = ('a -> ()) * (() -> 'a). Then given

* an instance of type `('a -> ()) * (() -> 'a)` and

* a function `f` of type `'a -> 'b`,

try to create an instance of type ('b -> ()) * (() -> 'b). You can't because ('a -> ()) * (() -> 'a) is invariant* in 'a.

Thanks. I wrote the following to convince myself of this:

let mapWrapDispatch
      (map: 'a -> 'b)
      (wrap: ('a -> unit) -> ('a -> unit))
      : (('b -> unit) -> ('b -> unit)) =
  fun dispatch msg ->
    IMPOSSIBLE

Since this will be a breaking change, I currently stand by my previous remark, though I'm open to other opinions.

If we need to make that breaking change with no migration path to identical functionality, we should announce that in an issue and request feedback, and leave that up for a while. And before that we should look at the existing discussions of the feature and any comments we have received there.

Furthermore, if we actually need to implement this ourselves, that requires some research, too, to make sure it's performant (both CPU and memory).

@TysonMN
Copy link
Member Author

TysonMN commented Aug 5, 2020

we should look at the existing discussions of the feature and any comments we have received there.

Do you mean the wrapDispatch feature or the mapMsg feature? I originally thought you meant wrapDispatch, but maybe I misunderstood. If you mean the wrapDispatch feature, then...

The issue that created the wrapDispatch feature is #114. Only you and I commented there.

Furthermore, if we actually need to implement this ourselves, that requires some research, too, to make sure it's performant (both CPU and memory).

You mean the throttling, debouncing, and limiting features, right? My hope is that we can delay this work. If no one is using the wrapDispatch feature, then we don't even need to provide a partial migration path (right away).

@cmeeren
Copy link
Member

cmeeren commented Aug 6, 2020

Do you mean the wrapDispatch feature or the mapMsg feature?

wrapDispatch.

You mean the throttling, debouncing, and limiting features, right?

Yep.

My hope is that we can delay this work. If no one is using the wrapDispatch feature, then we don't even need to provide a partial migration path (right away).

Yep, that's fine. We may want to request comments in an issue to be sure (as sure as we can, anyway). Alternatively we can just release v4 and take it from there if we get any comments. People don't have to upgrade right away, after all. Perhaps that's the best course of action? It certainly allows us to move faster. What do you think?

@TysonMN TysonMN mentioned this pull request Aug 6, 2020
9 tasks
@TysonMN
Copy link
Member Author

TysonMN commented Aug 6, 2020

Yep, that's fine. We may want to request comments in an issue to be sure (as sure as we can, anyway). Alternatively we can just release v4 and take it from there if we get any comments. People don't have to upgrade right away, after all. Perhaps that's the best course of action? It certainly allows us to move faster. What do you think?

Yes, I think moving faster would be ok. We can do a bit of both though. I created issue #253, which can be a source of discussion for both the proposed removal of the wrapDispatch feature as well as any other breaking change that will go into the next major version.

@cmeeren
Copy link
Member

cmeeren commented Aug 6, 2020

Great! I'm happy with that approach. Thanks for creating that issue.

@TysonMN
Copy link
Member Author

TysonMN commented Aug 9, 2020

This PR into master has been replaced with PR #256 into v4.

@TysonMN TysonMN closed this Aug 9, 2020
@cmeeren

This comment has been minimized.

@TysonMN

This comment has been minimized.

@cmeeren

This comment has been minimized.

@TysonMN

This comment has been minimized.

@cmeeren

This comment has been minimized.

@TysonMN TysonMN deleted the improve_SubModelSeq_Composability branch August 10, 2020 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants