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

Example use of mapModel and mapMsgWithModel #295

Closed
TysonMN opened this issue Nov 3, 2020 · 38 comments
Closed

Example use of mapModel and mapMsgWithModel #295

TysonMN opened this issue Nov 3, 2020 · 38 comments

Comments

@TysonMN
Copy link
Member

TysonMN commented Nov 3, 2020

The v4 branch includes the new features Bindings.mapModel and Bindings.mapMsgWithModel (and four other special cases). I have primarily argued that these are good features because they are motivated by the theory of functional programming. I have also tried to give example uses of these binding mapping functions, but I think more would helpful.

Consider our three Binding.subModelSeq methods. Here are the name and arguments of each of these methods from least expressive to most expressive (on the v4 branch).

static member subModelSeq
(getSubModels: 'model -> #seq<'subModel>,
getId: 'subModel -> 'id,
bindings: unit -> Binding<'model * 'subModel, 'msg> list)

static member subModelSeq
(getSubModels: 'model -> #seq<'subModel>,
getId: 'subModel -> 'id,
toMsg: 'id * 'subMsg -> 'msg,
bindings: unit -> Binding<'model * 'subModel, 'subMsg> list)

static member subModelSeq
(getSubModels: 'model -> #seq<'subModel>,
toBindingModel: 'model * 'subModel -> 'bindingModel,
getId: 'bindingModel -> 'id,
toMsg: 'id * 'bindingMsg -> 'msg,
bindings: unit -> Binding<'bindingModel, 'bindingMsg> list)

Of course the less expressive methods can be implemented by calling the more expressive methods. See here and here.

The interesting thing is that the more expressive methods can also be implemented by calling the less expressive methods with the help of Bindings.mapModel and Bindings.mapMsgWithModel. See here and here.

@cmeeren
Copy link
Member

cmeeren commented Nov 3, 2020

The interesting thing is that the more expressive methods can also be implemented by calling the less expressive methods with the help of Bindings.mapModel and Bindings.mapMsgWithModel. See here and here.

That's interesting. Here's a naïve question without much thought behind it: Does this mean the subModel subModelSeq binding can be simplified? Would it be a better user experience to removed the "more expressive" overloads and only keep the simplest variant, requiring users to use mapModel and mapMsgWithModel?

@TysonMN
Copy link
Member Author

TysonMN commented Nov 3, 2020

I was also thinking that. I "want" that to be true, but I don't think it is the case for either of our two bindings that are not the most expressive. Even though it was possible to implement the more expressive by the less expressive, neither implementation is something I want to put off on the user.

I wonder if a slightly different type of SubModelSeq binding would make things more composable. I hope this is true, but I don't know any reason why it should be. It is just a feeling I have at the moment.

@cmeeren
Copy link
Member

cmeeren commented Nov 3, 2020

I see. In any case, you are free to create another sample that showcases mapModel and mapMsgWithModel, if that was your intention with this issue.

@TysonMN
Copy link
Member Author

TysonMN commented Nov 3, 2020

My primary goal with this issue was just to share an example use of mapModel and mapMsgWithModel. My secondary goal is to document these particular uses of mapModel and mapMsgWithModel so that I don't forget them. The long-term goal is to maybe simplify the creation of SubModelSeq bindings, but I don't see how to do that right now.

@TysonMN

This comment has been minimized.

@cmeeren

This comment has been minimized.

@TysonMN
Copy link
Member Author

TysonMN commented Nov 17, 2020

A SubModelSeq binding is more expressive than a SubModel binding, so it should it not be surprising that this same equivalence also exists there.

  • The less expressive methods can be implemented by calling the more expressive methods. See here and here.
  • The more expressive methods can also be implemented by calling the less expressive methods with the help of Bindings.mapModel and Bindings.mapMsg. See here and here.

Furthermore, even the use of getSubModel can be done by Bindings.mapModel. For example, given...

static member subModel
(getSubModel: 'model -> 'subModel,
bindings: unit -> Binding<'model * 'subModel, 'msg> list)
: string -> Binding<'model, 'msg> =
SubModelData {
GetModel = fun m -> (m, getSubModel m) |> box |> ValueSome

...we can write

let mappedBindings : Binding<'model, 'msg> list =
  bindings () |> Bindings.mapModel (fun m -> (m, getSubModel m))

Only the essential behavior of a SubModel binding remains, which is to reduce a list of bindings to a single binding. This process isolates the names of those bindings in their own scope. The value behind the reduced binding is a new instance of ViewModel<_,_>, and it only makes sense for such a binding to be associated with the DataContext property of some FrameworkElement.

With this in mind, I think the right perspective is think about three tools that can be applied independently.

  1. Want a list of bindings to have a different model parameter? Then call Bindings.mapModel.
  2. Want a list of bindings to have a different message parameter? Then call Bindings.mapMsg (or Bindings.mapMsgWithModel).
  3. Want to reduce a list of bindings to a single binding? Then call Binding.subModel.

The most expressive overload of Binding.subModel does all three of these at once.

Even though it was possible to implement the more expressive [SubModelSeq bindings] by the less expressive [ones], neither implementation is something I want to put off on the user.

Unlike my current understanding of the Binding.SubModelSeq methods, I do think it would be reasonable to consider removing the two more expressive Binding.subModel methods. The migration path would be to use Bindinds.mapModel and Bindings.mapMsg instead. This would change a call like...

"Clock" |> Binding.subModel((fun m -> m.Clock), snd, ClockMsg, Clock.bindings)

...to

"Clock" |> Binding.subModel((fun m -> m.Clock), (fun () -> Clock.bindings () |> Bindings.mapModel snd |> Bindings.mapMsg ClockMsg)

Somewhat annoying is the difference between Binding list and () -> Binding list, but we can add map functions for that doubly-nested functor and put them in a module called FuncBindings so that it is possible to instead write

"Clock" |> Binding.subModel((fun m -> m.Clock), Clock.bindings |> FuncBindings.mapModel snd |> FuncBindings.mapMsg ClockMsg)

The obvious downside is that the syntax (in either case) is more verbose than the current Binding.subModel overloads. We effectively traded a single comma for ~20 characters. We have three functions to disambiguate (mapModel vs mapMsg vs mapMsgWithModel) on three functors (Binding vs Binding list in the module Bindings vs () -> Binding list in the module FuncBindings).

As a side note, higher-kinded types would help some with this verbose naming. I think it would allow us to drop the module names. In Haskell, mapMsg would become fmap (for a covariant functor), mapModel would become contramap (for a contravariant functor), and I don't know a theoretical understanding for mapMsgWithModel.

@cmeeren
Copy link
Member

cmeeren commented Nov 18, 2020

With this in mind, I think the right perspective is think about three tools that can be applied independently.

  1. Want a list of bindings to have a different model parameter? Then call Bindings.mapModel.

  2. Want a list of bindings to have a different message parameter? Then call Bindings.mapMsg (or Bindings.mapMsgWithModel).

  3. Want to reduce a list of bindings to a single binding? Then call Binding.subModel.

Thanks, that was helpful!


Somewhat annoying is the difference between Binding list and () -> Binding list, but we can add map functions for that doubly-nested functor and put them in a module called FuncBindings so that it is possible to instead write

I don't think we need that; from the looks of it, your middle code snippet can be rewritten to

"Clock" |> Binding.subModel((fun m -> m.Clock), Clock.bindings >> Bindings.mapModel snd >> Bindings.mapMsg ClockMsg)

We effectively traded a single comma for ~20 characters.

Shorter after Func is gone, but we could make mapModel and mapMsg available globally to avoid Bindings, too:

"Clock" |> Binding.subModel((fun m -> m.Clock), Clock.bindings >> mapModel snd >> mapMsg ClockMsg)

Existing overload for comparison:

"Clock" |> Binding.subModel((fun m -> m.Clock), snd, ClockMsg, Clock.bindings) 

I think the alternative is perfectly reasonable. In fact, I think it's more readable than the current overload, because then you have to remember what the different parameters are.


We have three functions to disambiguate (mapModel vs mapMsg vs mapMsgWithModel)

Didn't you say we could remove mapMsgWithModel in #253 (comment)? Then we're down to two functions.


Does the above change your view of whether we should remove the expressive subModel and subModelSeq overloads and have users use the more orthogonal API of mapModel and mapMsg instead?

@TysonMN
Copy link
Member Author

TysonMN commented Nov 18, 2020

Shorter after Func is gone, but we could make mapModel and mapMsg available globally to avoid Bindings, too

Two good ideas in a row. Great work! :)

Specifically, you are suggesting to make globally available the functions currently accessed as Bindings.mapModel and Bindings.mapMsg, which are both for the nested type Binding<'model, 'msg> list. In contrast, there are also functions Binding.mapModel and Binding.mapMsg, which are both for the type Binding<'model, 'msg>. Only one of each function can be made globally accessible, and I think you are correct to pick the ones for the nested type Binding<'model, 'msg> list. They are typically what is needed.

I think the alternative is perfectly reasonable. In fact, I think it's more readable than the current overload, because then you have to remember what the different parameters are.

I agree.

Furthermore, even the use of getSubModel can be done by Bindings.mapModel.

Because of that, we can do even better with this new API.

These two would be equivalent...

"Clock" |> Binding.subModel((fun m -> m.Clock), Clock.bindings)
"Clock" |> Binding.subModel(Clock.bindings >> mapModel (fun m -> (m, m.Clock)))

...where Clock.bindings : Binding<'model * 'subModel, 'msg> and these two would be equivalent...

"Clock" |> Binding.subModel((fun m -> m.Clock), snd, id, Clock.bindings)
"Clock" |> Binding.subModel(Clock.bindings >> mapModel (fun m -> m.Clock))

...where Clock.bindings : Binding<'subModel, 'msg>.

What I am emphasizing here is that our current simplest overload of Binding.subModel uses getSubModel : 'model -> 'subModel to map Binding<'model * 'subModel, 'msg> list to Binding<'model, 'msg>. When the user has full control over how the model is mapped with mapModel, then Binding.subModel doesn't need to pair up the 'model instance with the 'subModel instance as it currently does.

So the real comparison is between these two equivalent expressions...

"Clock" |> Binding.subModel((fun m -> m.Clock), snd, ClockMsg, Clock.bindings)
"Clock" |> Binding.subModel(Clock.bindings >> mapModel (fun m -> m.Clock) >> mapMsg ClockMsg)

...where the first line is currently in use. The second line is exactly what I meant when I listed the "three tools that can be applied independently" in #295 (comment). In order of execution, that line uses mapModel once, mapMsg once, and (a single argument version of) Binding.subModel once. So elegant!

If/when the feature suggestion fsharp/fslang-suggestions#506 is completed, then both lines can be simplified by replacing (fun m -> m.Clock) with something like _.Clock. For those like me that can't wait and so define their own getters (which I also do extensively in my code at work), both lines would simplify to something like...

"Clock" |> Binding.subModel(Clock.get, snd, ClockMsg, Clock.bindings)
"Clock" |> Binding.subModel(Clock.bindings >> mapModel Clock.get >> mapMsg ClockMsg)

...for which I find the lack of nested parentheses especially pleasing.

In Haskell, profunctors have the function dimap. If we also defined and globally exposed that function, then the last line could be further simplified to

"Clock" |> Binding.subModel(Clock.get, snd, ClockMsg, Clock.bindings)
"Clock" |> Binding.subModel(Clock.bindings >> dimap Clock.get ClockMsg)

We have three functions to disambiguate (mapModel vs mapMsg vs mapMsgWithModel)

Didn't you say we could remove mapMsgWithModel in #253 (comment)? Then we're down to two functions.

My general point there is that I want to be more cautious about expanding the API than I was when I introduced wrapDispatch.

I think about the syntax and semantics separately. Semantically, I am 100% positive about adding to the API the model-mapping and message-mapping concepts. I perfectly understand the theory: they are the mapping functions for contravariant and covariant functors respectively. At the same time, the syntax includes many options. I suggested |> FuncBindings.mapModel and you improved it once to >> Bindings.mapModel and again to >> mapModel (and the same for mapMsg). Maybe we also combine them into dimap as I pointed out above.

My specific point there is that I used to have an example use of mapMsgWithModel in the SubModelSeq sample, but I was able to simplify it to just mapMsg when I also made other simplifications to the sample as a whole.

But that is also the main reason I created this issue. Unlike the how I showed that the Binding.subModel overloads are equivalently expressive modulo mapModel and mapMsg in #295 (comment), I only showed what the Binding.subModelSeq overloads are equivalently expressive modulo mapModel and mapMsgWithModel (instead of mapMsg) in #295 (comment).

I still don't (fully) understand the theoretical justification for mapMsgWithModel. However, I am getting closer.

In functional programming, a profunctor is a bifunctor that is contravariant in its first type argument and covariant in its second one. The canonical example is the function type 'a -> 'b: it is contravariant in 'a and covariant in 'b. Our Binding<'model, 'msg> is a profunctor: it is contravariant in 'model and covariant in 'msg.

My implementation of mayMsgWithModel has the signature

('model -> 'subMsg -> 'msg) -> Binding<'model, 'subMsg> -> Binding<'model, 'msg>

Suppose we swap the "inputs" of the given function, make the type parameter names single letters, and replace the profunctor Binding<'a, 'b> with 'a -> 'b. Then we have

('b -> 'a -> 'c) -> ('a -> 'b) -> ('a -> 'c)

We can compose 'a -> 'b with 'b -> 'a -> 'c to get 'a -> 'a -> 'c and flatten that (as I learned in July) to get 'a -> 'c. Of course map f >> flatten is the same as bind f and function composition is the (covariant) mapping function of the profunctor 'a -> 'b, so that signature above is the signature of bind for the type 'a -> 'b that is monadic in 'b.

That argument is completely valid for the profunctor 'a -> 'b, but it only applies to the profunctor Binding<'model, 'msg> as a heuristic or approximation. As such, it suggestions (but does not imply) that Binding<'model, 'msg> is monadic in 'msg, but is it really? I don't know. My intuition is that this is not the case, but I hope I am wrong! In general, it is simple to show that something is monadic but difficult to show that something is not monadic.

(As an example of an argument showing that something isn't monadic, consider a pair 'a * 'b. It is a bifunctor that is covariant in both type parameters. Is it also monadic in 'b (and by symmetry 'a)? If it were, then it would be possible to flatten 'a * ('a * 'b) to 'a * 'b. We have two instances of 'a and no way to combine them, so we have to keep one or the other. Intuitively, this feels like a bad implementation. Indeed, one can confirm that this intuition is correct by giving an example that violates the monadic laws in each case. So when someone asks you for an example of a functor that is not a monad, you can point to the pair type. In that case, you can also consider replacing 'a with a concrete type so that the resulting type only has a single type parameter. I recommend Guid. Be careful if using a different type because it can yield a type that is monadic. The simplest example of that is unit).


Does the above change your view of whether we should remove the expressive subModel and subModelSeq overloads and have users use the more orthogonal API of mapModel and mapMsg instead?

My heart absolutely wants this. My head is trying to be cautious.

I think your suggestions improved the syntax of the mapping functions enough that I am onboard with deprecating the existing overloads of Binding.subModel and adding a new overload that just maps Binding<'model, 'msg> list to Binding<'model, 'msg>.

I can't (yet) say the same for the Binding.subModelSeq overloads. The main reason is because the equivalence I showed needs mapMsgWithModel. That case is also more complicated because of the IDs. I have to think about it more.

@cmeeren
Copy link
Member

cmeeren commented Nov 18, 2020

"Clock" |> Binding.subModel(Clock.bindings >> dimap Clock.get ClockMsg)

Personally I don't find this much better than

"Clock" |> Binding.subModel(Clock.bindings >> mapModel Clock.get >> mapMsg ClockMsg)

I'm not necessarily strongly opposed to adding a dimap function (mapModelAndMsg?), but I don't really see the usefulness for users. It's trivial to define if they really want it:

let mapModelAndMsg fModel fMsg = mapModel fModel >> mapMsg fMsg

In functional programming, a profunctor is a bifunctor that is contravariant [...]

This whole part is all quite a bit above my understanding. What I'm wondering about, as usual, is whether there is anything useful mapMsgWithModel allows users to do that can not be accomplished as easily (or at all) otherwise.


That case is also more complicated because of the IDs. I have to think about it more.

Think away and let me know if you need any input. :)

@TysonMN
Copy link
Member Author

TysonMN commented Nov 18, 2020

I am also fine with not adding dimap or mapModelAndMsg.


mapMsgWithModel is definitely more expressive than mapMsg. What remains unknown is if this additional expressiveness is practically useful.

@cmeeren
Copy link
Member

cmeeren commented Nov 18, 2020

mapMsgWithModel is definitely more expressive than mapMsg. What remains unknown is if this additional expressiveness is practically useful.

If I have understood mapMsgWithModel correctly, it's a mapMsg that also takes the current model.

If that is correct, then I don't think it's useful; in fact it can be confusing. AFAIK the only thing you can achieve is that you can use the current model state to decide which message you want to send. But I don't see anything you can do when you create the message that you also can't do in update, where you of course also have access to the current model. The "confusing" aspect is then the fact that there are two ways to achieve the same thing, and of those two, I prefer keeping all the logic in update.

Though as I'm writing this, it strikes me that for a subModelSeq binding, you only know which particular sub-model you have when you are constructing the message, and the sub model's ID usually needs to be part of the message. So you'd need mapMsgWithModel for subModelSeq bindings. Is my reasoning correct?

@TysonMN
Copy link
Member Author

TysonMN commented Nov 18, 2020

If I have understood mapMsgWithModel correctly, it's a mapMsg that also takes the current model.

Yes, that is the correct understanding.

Though as I'm writing this, it strikes me that for a subModelSeq binding, you only know which particular sub-model you have when you are constructing the message, and the sub model's ID usually needs to be part of the message. So you'd need mapMsgWithModel for subModelSeq bindings. Is my reasoning correct?

Yes, that reasoning is correct. I had considered pointing this out in a previous comment, but it was already long enough :P And is it good that you figured that out yourself "the hard way" ;)

Certainly something like that is true.

Of course option and seq are different types, both are monads, but it is reasonable to say that option is simpler. Now that I think I completely understand the Binding.subModel methods, I think the next methods to consider are Binding.subModelOpt instead of Binding.sumModelSeq.

@TysonMN
Copy link
Member Author

TysonMN commented Nov 20, 2020

When I created this issue, I didn't realize (until now) that the ideas I am sharing are making progress on achieving the composable binding API described in #263.

@cmeeren
Copy link
Member

cmeeren commented Nov 20, 2020

it is reasonable to say that option is simpler.

Yes, I think specifically (in this case) in the fact that you do not need to differentiate between items. It has 0 or 1 item.

@TysonMN
Copy link
Member Author

TysonMN commented Nov 20, 2020

Furthermore, Binding.oneWayOpt is simpler than Binding.subModelOpt.

I think I see how all our binding methods decompose and become composable. Now I just need to find the time to try and implement them.

@cmeeren
Copy link
Member

cmeeren commented Nov 20, 2020

Wonderful, looking forward to seeing what comes out of this eventually!

@TysonMN
Copy link
Member Author

TysonMN commented Nov 25, 2020

Partially motivated by "the" theory of mapMsgWithModel (and not by a practical use), I previously suggested in issue #243 that the Binding.subModeWin methods could provide the model to the user as they pick which message should be dispatched when a close of the window is attempted. Here is something related to that.

I feel like the Binding.subModelOpt methods have an annoying asymmetry. Consider this one.

static member subModelOpt
(getSubModel: 'model -> 'subModel option,
toMsg: 'subMsg -> 'msg,
bindings: unit -> Binding<'model * 'subModel, 'subMsg> list,
?sticky: bool)

Like most binding types, there is a two-way street there. One direction is from the model to WPF. This is represented by getSubModel. In the other direction, messages describing how the model should be changed are sent from WPF to the Elmish dispatch loop. This is represented by toMsg.

When the value returned by getSubModel is in the None state (and the binding is not sticky), then null is provided to WPF. As a user, that is convenient.

The road in the other direction is not as convenient. Whether getSubModel returns an option in the Some or None state, the toMsg function always needs to be able to map 'subMsg to 'msg. However, it is not possible for this function to be executed when getSubModel returns None. When the 'msg is handled in the corresponding update function, the corresponding 'model will typically have a field of type 'subModel option. Presumably it is in the Some state since this message couldn't have been dispatched, but now we have to handle both cases.

I think it would be interesting to consider an overload of Binding.subModelOpt in which toMsg has the type 'subModel -> 'subMsg -> 'msg. That is, this message mapping function is given access to the 'subModel instance without it being wrapped in with option.

This reminds me of how this overload of Binding.cmdIf works.

static member cmdIf
(exec: 'model -> 'msg option,
?wrapDispatch: Dispatch<'msg> -> Dispatch<'msg>)

In practice, I often create exec by first calling a function like getSubModel : 'model -> 'subModel that takes in the 'model and then optionally mapping (via Option.map) a function like toMsg : 'subMsg -> msg that returns the message that I want dispatched when the button is clicked.

One downside with this idea is that messages become less commutative. Here is a quick example to show what I mean by messages being commutative. Suppose a button toggles the value of a Boolean. The commutative way to toggle it is to dispatch a toggle message and then implement update for that message like

| ToggleMyBool -> { m with MyBool = not m.MyBool }

The non-commutative way to toggle it is to dispatch a message like

Binding.cmd (fun m -> m.MyBool |> not |> SetMyBool)

and then implement update for that message like

| SetMyBool b -> { m with MyBool = b }

Now I said all that to say this.

I think the user will be able to pick between these two approaches in the composable binding API that I currently envision and think is possible. Trying to just convey the high-level picture and ignoring the details (which is hard for me!), I think it would look something like this.

Suppose with have

let subBinings : Binding<'subModel, 'subMsg> list = ...

Then the current behavior of Binding.subModelOpt would be something like

subBinings
|> mapMsg toMsg
|> Binding.opt
|> mapModel getSubModel

The less commutative approach would be something like

subBinings
|> mapMsgWithModel toMsg
|> Binding.opt
|> mapModel getSubModel

Of course the difference there is the toMsg is given access to subModel before it gets wrapped with option by Binding.opt.

And that is also why I decided to share this idea as a comment in this issue: because it is an example of how one could use mapMsgWithModel.

@TysonMN
Copy link
Member Author

TysonMN commented Nov 27, 2020

Then we have

('b -> 'a -> 'c) -> ('a -> 'b) -> ('a -> 'c)

I now think such a function does not exist for all profunctors. In addition to not finding any matching theory for profunctors, I think the following is a counterexample. Though, as I said before, it is difficult to prove that something doesn't exist.

type P<'a, 'b> =
  { In: 'a -> unit
    Out: unit -> 'b }

This is a profunctor and I don't see how to implement

('b -> 'a -> 'c) -> P<'a, 'b> -> P<'a, 'c>

@cmeeren
Copy link
Member

cmeeren commented Nov 29, 2020

When the 'msg is handled in the corresponding update function, the corresponding 'model will typically have a field of type 'subModel option. Presumably it is in the Some state since this message couldn't have been dispatched, but now we have to handle both cases.

Correct, both cases must be accounted for. Though (since they are both functors) not unlike you would have to handle "all" of the cases if you had a list instead of an option. You likely just use a map anyway. I don't see any way around that.

I think it would be interesting to consider an overload of Binding.subModelOpt in which toMsg has the type 'subModel -> 'subMsg -> 'msg. That is, this message mapping function is given access to the 'subModel instance without it being wrapped in with option.

I don't see how this avoids update having to deal with the None case, since generally, the message represents something to be updated in the in the sub-model, which may always be None. The only difference I can see between the two approaches is that the message can contain information from the sub-model. But update will still have to discard it if the sub-model is None, so this information isn't useful.

IMHO it's better to have slim messages and have update handle taking into account the existing state, but (as always) I'm open to being challenged on that, if there are use-cases that become simpler the other way around.

One downside with this idea is that messages become less commutative.

I think the word you're looking for is idempotent (or, simplified, "retryable"). AFAIK the definition of commutative doesn't fit here, since it is about the result of an operation on two items from a set being invariant with respect to the order in which those items are used in the operation.

I think the user will be able to pick between these two approaches in the composable binding API that I currently envision and think is possible.

I see, thanks.

@TysonMN
Copy link
Member Author

TysonMN commented Dec 3, 2020

I think it would be interesting to consider an overload of Binding.subModelOpt in which toMsg has the type 'subModel -> 'subMsg -> 'msg. That is, this message mapping function is given access to the 'subModel instance without it being wrapped in with option.

I don't see how this avoids update having to deal with the None case, since generally, the message represents something to be updated in the in the sub-model, which may always be None.

If the binding's setter is given an instance of subModel, then the binding's setter could be implemented like

fun subModel msg -> SubModelMsg(subModel, msg)

and the update case implemented as

| SubModelMsg(subModel, msg) -> { m with SubModel = subModel |> SubModel.update msg |> Some }

One downside with this idea is that messages become less commutative.

I think the word you're looking for is idempotent (or, simplified, "retryable").

You are correct to notice the idempotency involved. There is also commutativity involved.

Let f = g = ToggleMyBool |> update. Then f >> g = g >> f. Thus, I say that ToggleMyBool is a commutativite message. It is not an idempotent message because f <> f >> f.

Let f = false |> SetMyBool |> update and g = true |> SetMyBool |> update. Then f >> g <> g >> f. Thus, I say that SetMyBool is not a commutative message (with itself). It is an idempotent message because f = f >> f and g = g >> g.

I actually said "less commutative" though. By less commutative, I mean ToggleMyBool commutes with the other message cases and itself while SetByBool commutes with the other message cases but not itself.

@TysonMN
Copy link
Member Author

TysonMN commented Dec 3, 2020

IMHO it's better to have slim messages and have update handle taking into account the existing state, but (as always) I'm open to being challenged on that, if there are use-cases that become simpler the other way around.

Me too. Mathematically speaking, I think this typically leads to messages that are more commutative. That is definitely the case with ToggleMyBool vs SetMyBool.

I don't have a compelling example to share, but I had something similar to ToggleByBool in my application at work. I changed it to something similar to SetMyBool even though that is less commutative because I thought it was a good tradeoff with how much simpler the code became.

@cmeeren
Copy link
Member

cmeeren commented Dec 3, 2020

If the binding's setter is given an instance of subModel, then the binding's setter could be implemented like

fun subModel msg -> SubModelMsg(subModel, msg)

and the update case implemented as

| SubModelMsg(subModel, msg) -> { m with SubModel = subModel |> SubModel.update msg |> Some }

Hm. First, it sounds like that will lead to very noisy logs if you're logging messages. Second, you then let sub-model messages control whether the sub-model is present or not. There may be cases where you don't want e.g. a sub-window/modal/whatever to pop up just because some background HTTP request finally came through after you closed the window.

Again, I still prefer slim messages.

Also, I don't find your code examples to be any simpler than the following (which are even shorter):

SubModelMsg  // shorter version of fun msg -> SubModelMsg msg
| SubModelMsg msg -> { m with SubModel = m.SubModel |> Option.map (SubModel.update msg) }

Let f = g = ToggleMyBool |> update. Then f >> g = g >> f. Thus, I say that ToggleMyBool is a commutativite message.

Thanks for the clarification. I think I understand this point now: The state is equal after receiving two messages of a certain type no matter the order they are received in. However, I fail to see how that is a useful property in this context. Does it make anything simpler? (I know you said you "don't have a compelling example to share", so I guess it's more a rhetorical question for now, unless you have some general clarifications.)

@TysonMN
Copy link
Member Author

TysonMN commented Dec 4, 2020

All good points above.

I think I understand this point now: The state is equal after receiving two messages of a certain type no matter the order they are received in. However, I fail to see how that is a useful property in this context. Does it make anything simpler? (I know you said you "don't have a compelling example to share", so I guess it's more a rhetorical question for now, unless you have some general clarifications.)

In general, commutativity is helpful because it makes things easier to reason about. The intention is that update is pure, but each output is assigned to a mutable reference.

Generally speaking, we all know the dangers of mutability. That is why we are using Elmish.WPF.

I have a 3-part series of blog posts in the queue about mutability. Here are main points of each post.

  1. Write pure code.
  2. If you write impure code; make it idempotent.
  3. If you write impure code twice; make them commutative.

It is easier to reason about pure code than impure code.
It is easier to reason about impure code that is idempotent than impure code that is not.
It is easier to reason about pairs of impure code that are commutative than pairs of impure code that are not.

Many people recommend the writing of pure code. I don't see many people recommending how to write "good" impure code. I want to help change that.

@cmeeren
Copy link
Member

cmeeren commented Dec 4, 2020

  1. Write pure code.
  2. If you write impure code; make it idempotent.
  3. If you write impure code twice; make them commutative.

Interesting. I believe you are right on all of them. As for 3., I understand that to mean (perhaps not generally enough) that you should seek to minimize interdependent fields in your model, interdependencies meaning that two operations are not commutative (changing A and then B would produce a different result than changing B and then A). Two immediate thoughts:

  • This is not related to impurity per se, and simplifies pure domain logic, too: Keeping interdependencies low makes it easier to reason about any single part of the code in isolation. (Or, to put it another way: If you consider Elmish partly mutable since each update output is assigned a mutable reference, you have to consider basically everything mutable since your otherwise immutable domain logic generally results in a database being updated somewhere.)
  • SetBool, despite not being commutative with itself given different message values, does not cause any added complexity over ToggleBool. On the contrary; when handling ToggleBool, the existing state needs to be taken into account, whereas when handling SetBool, it does not matter what the current state is. I find the latter simpler.

So I still don't get why commutativity is useful for a single type of operation (with different parameters). For example, I have a lot of "immutable setters" (typically a record copy-and-update) of the type param -> 'entity -> 'entity. These functions (operations) are idempotent and very simple to understand, but not commutative with themselves given different 'param values. They are, often, commutative with other operations (e.g. other immutable setters), but as described above, I don't see why them being commutative with themselves would make things any simpler.

Looking forward to your blog post (or further clarifications/discussions here – it's somewhat off topic, but given that we're the only ones currently involved in this issue, I care more about a good discussion and learning opportunity than keeping the issue strictly on-topic.)

@TysonMN
Copy link
Member Author

TysonMN commented Dec 4, 2020

Looking forward to your blog post (or further clarifications/discussions here – it's somewhat off topic, but given that we're the only ones currently involved in this issue, I care more about a good discussion and learning opportunity than keeping the issue strictly on-topic.)

Oh, I am very good at going off topic 🤣

  • SetBool, despite not being commutative with itself given different message values, does not cause any added complexity over ToggleBool.

One way it is more complex that you pointed out above is that the log entry for SetBool is larger than ToggleBool because it contains data.

  • This is not related to impurity per se, and simplifies pure domain logic, too

Suppose dispatch took a list of messages instead of just a single one and processed the whole list (via List.fold) before mutating the reference to the current model. Let m1 and m2 be messages. Then

dispatch [ m1 ]
dispatch [ m2 ]

mutates twice but is otherwise the same as

dispatch [ m1; m2 ]

which mutates once, in the sense that the final state of the model is the same in both cases.

My future blog post will frame things in terms of impurity, and I am pointing out the connection between impurity and dispatch / update. However, you are right that idempotency and commutativity are useful properties to have even for pure code.

So I still don't get why commutativity is useful for a single type of operation (with different parameters).

You might be right about this. In particular, it might be more important for a single message with the same data to be idempotent than for two instances of the same message with different data to commute.

@cmeeren
Copy link
Member

cmeeren commented Dec 4, 2020

One way it is more complex that you pointed out above is that the log entry for SetBool is larger than ToggleBool because it contains data.

I agree, though it is only trivially larger. In this particular case, I don't think that matters. And if there was another case where the data was larger, then the whole point would likely be moot because there could be no Toggle variant (I can't think of anything other than a bool toggle this is relevant for).

You might be right about this. In particular, it might be more important for a single message with the same data to be idempotent than for two instances of the same message with different data to commute.

Yes, I believe that this can make update logic simpler. Though admittedly I am currently only thinking about updates similar to the "immutable setters" I mentioned, which may be a limited view of the kinds of updates there are. I am not sure the principle applies generally, and I am happy to be proven wrong.

@TysonMN
Copy link
Member Author

TysonMN commented Dec 6, 2020

Quick post.

First, I figured out how to make the model optional. Just call mapModel Object.toObj. See this commit (for which all tests pass).

Second, I was a bit wrong about expressing the more complicated overloads of Binding.subModel and Binding.subModelSeq in terms of the simplest overload (in each case). Some tests fail. I still think it is possible though. Need to investigate further.

@TysonMN
Copy link
Member Author

TysonMN commented Dec 6, 2020

Interesting (at least to me) is that (I think) the implementation of the optional one-way bindings in that commit are more like

module Option =

  let box ma = ma |> Option.toObj |> box
  let unbox obj = obj |> unbox |> Option.ofObj

module ValueOption =

  let box ma = ma |> ValueOption.toObj |> box
  let unbox obj = obj |> unbox |> ValueOption.ofObj

instead of the current implementation of

module Option =
let box ma = ma |> Option.map box |> Option.toObj
let unbox obj = obj |> Option.ofObj |> Option.map unbox
module ValueOption =
let box ma = ma |> ValueOption.map box |> ValueOption.toObj
let unbox obj = obj |> ValueOption.ofObj |> ValueOption.map unbox

(Both definitions pass all tests.)

@TysonMN
Copy link
Member Author

TysonMN commented Dec 7, 2020

Second, I was a bit wrong about expressing the more complicated overloads of Binding.subModel and Binding.subModelSeq in terms of the simplest overload (in each case). Some tests fail. I still think it is possible though. Need to investigate further.

I figured out the problem for the Binding.subModel overloads. I am confident that the Binding.subModelSeq overloads have the same issue, but they are more complicated and I don't understand them as well, so I have not verified that it really is the same issue.

I forced pushed my branch investigate/Binding.subModel after rebasing on v4 and adding two commits (which I will directly link to below).

I believe the problem is that the SubModel tests are overly specific. I think they not only verify that the behavior is correct but also stipulate some details of the implementation.

First consider this commit. The tests pass for that commit. I did have to change the tests in a rather trivial way though. I replaced some references of fail with id for the functions that do model mapping. This is necessary because the ToMsg field has type 'model -> 'bindingMsg -> 'msg and after each of the calls to Binding.mapModel (here is one), the new ToMsg field invokes the given getSubModel and toBindingModel arguments. (I could avoid changing the tests if BindingData.mapMsgWithModel didn't exist.)

Now consider this commit. All I did was change the order. Recall the three fundamental concepts currenting combined in our two least completed Binding.subModel overloads.

  • Want a list of bindings to have a different model parameter? Then call Bindings.mapModel.
  • Want a list of bindings to have a different message parameter? Then call Bindings.mapMsg (or Bindings.mapMsgWithModel).
  • Want to reduce a list of bindings to a single binding? Then call Binding.subModel.

I changed the order of those three steps. In the previous commit, the order was 3, 1, 2. In the current commit, the order is 1, 2, 3. Nonetheless, there are some failing tests. They are failing because they are overly specific. One way to confirm this is to run the SubModel sample and observe that it works perfectly.

All six permutations of these three steps should be functionally equivalent. Order shouldn't matter. I struggled to figure out why the tests were failing because I assumed that order wouldn't matter.

Going forward, I will leave the tests as they are and continue to focus on the Binding overloads while using whatever order the tests want. Once I have the API figured out, I will remove duplicate tests and then change the remaining tests so that they are not too strict.


One other thing to observe is that when I changed the order, I also had to change one other character. I had to change Binding to Bindings. Here is an case of that reordering.

// one order (original order for which tests failed)
bindings
>> Bindings.mapModel (fun m -> (m, getSubModel m))
|> Binding.subModel

// another order (new order for which tests pass)
bindings
|> Binding.subModel
>> Binding.mapModel (fun m -> (m, getSubModel m))

Previously, because I had used that original order, we thought globally exposing Bindings.mapModel was a good idea. However, I think the fact that all six permutations are equally valid suggests that neither Bindings.mapModel nor Binding.mapModel is more important than the other.

Technically, I would say that Binding.mapModel is more fundamental since Bindings.mapModel is actually defined via it. However, each can be defined by the other.

module Binding =
  let mapModel f binding = [ binding ] |> Bindings.mapModel f
module Bindings =
  let mapModel f bindings = bindings |> List.map (Binding.mapModel f)

Even so, we could still pick one to globally expose as a convenience, which will lead to a preference for certain orderings, which is technically fine since they are equally good.

(Of course, the same goes for mapMsg and mapModelWithMsg.)

This naming issue isn't very important right now. I just wanted to point this out given the relevant example.

@cmeeren
Copy link
Member

cmeeren commented Dec 8, 2020

Going forward, I will leave the tests as they are and continue to focus on the Binding overloads while using whatever order the tests want. Once I have the API figured out, I will remove duplicate tests and then change the remaining tests so that they are not too strict.

Feel free to change the tests anytime.

Even so, we could still pick one to globally expose as a convenience, which will lead to a preference for certain orderings, which is technically fine since they are equally good.

I haven't yet experimented with these functions, but in general, I think that we should choose whatever provides the best API for users. If this really is just a question of ordering, it's not critical, but we should think about it all the same.

I am (as usual) a bit confused about the mix between >> and |>. I know perfectly well what it means when I stop to think about it, but I always have to stop and think about it. Is there a simple way to design the API to avoid that, and e.g. just use |>?

@TysonMN
Copy link
Member Author

TysonMN commented Dec 8, 2020

I haven't yet experimented with these functions, but in general, I think that we should choose whatever provides the best API for users. If this really is just a question of ordering, it's not critical, but we should think about it all the same.

I am (as usual) a bit confused about the mix between >> and |>. I know perfectly well what it means when I stop to think about it, but I always have to stop and think about it. Is there a simple way to design the API to avoid that, and e.g. just use |>?

I like to think that there is some essential complexity and some accidental complexity. (Though maybe that is a false dichotomy.) In what follows, I will be very verbose. I know you know much of it, but I find the verbosity helpful for finding all our assumptions.

First, we need some way to expose some procedure / logic / mapping / behavior. In F#, this can be done with a function or with a method. One advantage of using a function is the stronger type inference. One advantage of a method is the ability to overload (including via optional arguments). The idiomatic solution in F# is to use a function. Two functions with the same name on arguments of different types (that could be overloads of a single method) are made distinct using modules.

For example, consider the two equivalent expressions.

sequence
|> Seq.filter predicate
|> Seq.tryHead

sequence
|> Seq.tryHead
|> Option.filter predicate

There are two functors involved there: Seq and Option. The two functions named filter are distinguished by the modules named Seq and Option after their corrsponding functor. Higher-kinded types (that F# lacks and Haskell has) make this distinction unnecessary.

In our code, there are (at least) four functors involved:

  1. BindingData<'model, 'msg> (contravariant in 'model and covariant in 'msg),
  2. Binding<'model, 'msg> (contravariant in 'model and covariant in 'msg),
  3. List<'a> (covariant in 'a`), and
  4. 'a -> 'b (contravariant in 'a and covariant in 'b).

The first one (BindingData<'model, 'msg>) currently has internal scope. The other three have public scope.

The complexity takes a jump when we compose these functors to get make additional functors. (For simplicity, I will just focus on the 'model type parameter.)

Sometimes we have a list of bindings (with type List<Binding<'model, 'msg>>, which is using the prefix notation for generic types with a single type parameter, which I will do here for consistency). We can map 'model before or after turning that list into a single binding.

// bindings : List<Binding<'model, 'msg>>

bindings
|> List.map (Binding.mapModel f)
|> Binding.subModel

bindings
|> Bindings.mapModel f
|> Binding.subModel

bindings
|> Binding.subModel
>> Binding.mapModel f

Two things there. First, Bindings.mapModel (from the second expression) is defined precisely as in the first expression just to provide an easier way to map that composite functor. Second, the reason >> shows up in the third expression is because Binding.subModel returns string -> Binding<'model, 'msg>.

An alternative there is that Binding.subModel could return BindingData<'model, 'msg> (after making that type public) and some other function could combine a string and BindingData<'model, 'msg> into a Binding<'model, 'msg>. That could look like the second of these two equivalent expressions.

// bindings : List<Binding<'model, 'msg>>

bindings
|> Binding.subModel // returns string -> Binding<_, _>
>> Binding.mapModel f
<| "bindingName"

bindings
|> Binding.subModel // returns BindingData<_, _>
|> BindingData.mapModel f
|> BindingData.withName "bindingName"

This might be a bit of an unfair comparison since user code currently looks more like

"bindingName"
|> Binding.subModel bindings
|> Binding.mapModel f

I really like the aspect of the current API that puts all the binding name strings at the beginning of each line. Each binding name is a magic string, so unified placement of all the magic strings on the left of the screen is somewhat comforting. Maybe this isn't so crucial though.

As I decompose our current binding API into minimal composable pieces, I could express that directly instead of having the more expressive overloads call the less expressive ones. Then I would mostly use just the BindingData<'model, 'msg> and List<'a> functors when mapping.

In addition to List<Binding<'model, 'msg>>, it is also common to have () -> List<Binding<'model, 'msg>>. You informed me (quite a while ago now) that the purpose of this extra layer of indirection is to support recursive bindings. I find this somewhat satisfying that such a rare use case dictates the type that is used in all cases. (I don't have any recursive binding in my very large application at work.)

One idea is to back all the way up and consider using methods instead of functions to express the mappings for these (possibly composite) functors. This is the approach you took with the binding API in #87.

I think I have run out of ideas. Hopefully this discussion helps.


I think we are on the same page, but just to make sure, I will partially repeat myself by saying this.

We want our API to include good names and types, so that is why we are discussing this. However, we don't need them yet. I am still trying to figure out how to expose all the functionality we want in minimal composable pieces. Once I have all of those pieces, we can actually play with the code and find good names, good types (e.g. picking among the four functors I listed above or composites thereof), and good places for the mapping code to live (i.e. function vs method and module or class structure).

I will primarily continue to work on making these minimal composable pieces of functionality.

@cmeeren
Copy link
Member

cmeeren commented Dec 13, 2020

Thanks for the thorough explanation.

I really like the aspect of the current API that puts all the binding name strings at the beginning of each line. Each binding name is a magic string, so unified placement of all the magic strings on the left of the screen is somewhat comforting.

I agree and consider it important that we keep it that way.

This might be a bit of an unfair comparison since user code currently looks more like

"bindingName"
|> Binding.subModel bindings
|> Binding.mapModel f

Well that looks excellent. Name first, only one module, and only |> (not >>).

We want our API to include good names and types, so that is why we are discussing this. However, we don't need them yet. I am still trying to figure out how to expose all the functionality we want in minimal composable pieces. Once I have all of those pieces, we can actually play with the code and find good names, good types (e.g. picking among the four functors I listed above or composites thereof), and good places for the mapping code to live (i.e. function vs method and module or class structure).

I will primarily continue to work on making these minimal composable pieces of functionality.

Indeed 👍 Thank you for your excellent and ongoing work. I really look forward to the bindings being more composable. As always, holler if you need any input.

@TysonMN
Copy link
Member Author

TysonMN commented Dec 18, 2020

Here is a great example of the progress I have made so far.

I made "stickiness" into an effect that is composable. Currently in master, only a SubModelOpt binding has the possibility of being sticky. Now any binding can be sticky.

Here is a video that shows the counter value being sticky to even numbers. Try it for yourself in the branch composable/Sticky/example.

2020-12-18_08-13-37_433

@cmeeren
Copy link
Member

cmeeren commented Dec 18, 2020

Wonderful! 👏

@TysonMN
Copy link
Member Author

TysonMN commented Dec 18, 2020

Interesting (at least to me) is that (I think) the implementation of the optional one-way bindings in that commit are more like

module Option =

  let box ma = ma |> Option.toObj |> box
  let unbox obj = obj |> unbox |> Option.ofObj

module ValueOption =

  let box ma = ma |> ValueOption.toObj |> box
  let unbox obj = obj |> unbox |> ValueOption.ofObj

instead of the current implementation of

module Option =
let box ma = ma |> Option.map box |> Option.toObj
let unbox obj = obj |> Option.ofObj |> Option.map unbox
module ValueOption =
let box ma = ma |> ValueOption.map box |> ValueOption.toObj
let unbox obj = obj |> ValueOption.ofObj |> ValueOption.map unbox

(Both definitions pass all tests.)

I don't have optional bindings correct yet. My implementation adds the null constraint. There are no samples that use the any of the Binding.oneWayOpt overloads and the three calls in the tests didn't break when adding the null constraint. The same implementation doesn't work for the Binding.subModelOpt overloads. There is a sample for that, and the corresponding (record) type doesn't satisfy the null constraint.

I think I have to implement an optional binding like SubModelOpt so that a null constraint is not added.

@cmeeren
Copy link
Member

cmeeren commented Dec 18, 2020

I'm not exactly sure what you mean (you don't have to go into details), but I'd just like to suggest that if you have problems with null constraints you want to get rid of, a sprinkle of box/unbox may fix that.

@TysonMN
Copy link
Member Author

TysonMN commented Dec 18, 2020

I'm not exactly sure what you mean (you don't have to go into details)...

Yep...many of these comments are also for me. Either forcing myself to better understand or helping a future version of myself remember the state of my work.

I'd just like to suggest that if you have problems with null constraints you want to get rid of, a sprinkle of box/unbox may fix that.

Yes, I tried some of that but none of it was correct. Maybe I need to use Option.map like the code I quoted above.

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

No branches or pull requests

2 participants