-
-
Notifications
You must be signed in to change notification settings - Fork 71
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
Comments
That's interesting. Here's a naïve question without much thought behind it: Does this mean the |
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 |
I see. In any case, you are free to create another sample that showcases |
My primary goal with this issue was just to share an example use of |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
A
Furthermore, even the use of Elmish.WPF/src/Elmish.WPF/Binding.fs Lines 1146 to 1151 in d91c479
...we can write let mappedBindings : Binding<'model, 'msg> list =
bindings () |> Bindings.mapModel (fun m -> (m, getSubModel m)) Only the essential behavior of a With this in mind, I think the right perspective is think about three tools that can be applied independently.
The most expressive overload of
Unlike my current understanding of the
...to "Clock" |> Binding.subModel((fun m -> m.Clock), (fun () -> Clock.bindings () |> Bindings.mapModel snd |> Bindings.mapMsg ClockMsg) Somewhat annoying is the difference between "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 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, |
Thanks, that was helpful!
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)
Shorter after "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.
Didn't you say we could remove Does the above change your view of whether we should remove the expressive |
Two good ideas in a row. Great work! :) Specifically, you are suggesting to make globally available the functions currently accessed as
I agree.
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" |> Binding.subModel((fun m -> m.Clock), snd, id, Clock.bindings)
"Clock" |> Binding.subModel(Clock.bindings >> mapModel (fun m -> m.Clock)) ...where What I am emphasizing here is that our current simplest overload of 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 If/when the feature suggestion fsharp/fslang-suggestions#506 is completed, then both lines can be simplified by replacing "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 "Clock" |> Binding.subModel(Clock.get, snd, ClockMsg, Clock.bindings)
"Clock" |> Binding.subModel(Clock.bindings >> dimap Clock.get ClockMsg)
My general point there is that I want to be more cautious about expanding the API than I was when I introduced 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 My specific point there is that I used to have an example use of But that is also the main reason I created this issue. Unlike the how I showed that the I still don't (fully) understand the theoretical justification for 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 My implementation of
Suppose we swap the "inputs" of the given function, make the type parameter names single letters, and replace the profunctor
We can compose That argument is completely valid for the profunctor (As an example of an argument showing that something isn't monadic, consider a pair
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 I can't (yet) say the same for the |
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 let mapModelAndMsg fModel fMsg = mapModel fModel >> mapMsg fMsg
This whole part is all quite a bit above my understanding. What I'm wondering about, as usual, is whether there is anything useful
Think away and let me know if you need any input. :) |
I am also fine with not adding
|
If I have understood
Though as I'm writing this, it strikes me that for a |
Yes, that is the correct understanding.
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 |
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. |
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. |
Furthermore, 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. |
Wonderful, looking forward to seeing what comes out of this eventually! |
Partially motivated by "the" theory of I feel like the Elmish.WPF/src/Elmish.WPF/Binding.fs Lines 1309 to 1313 in d91c479
Like most binding types, there is a two-way street there. One direction is from the model to WPF. This is represented by When the value returned by The road in the other direction is not as convenient. Whether I think it would be interesting to consider an overload of This reminds me of how this overload of Elmish.WPF/src/Elmish.WPF/Binding.fs Lines 900 to 902 in d91c479
In practice, I often create 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 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 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 |
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
|
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
I don't see how this avoids IMHO it's better to have slim messages and have
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 see, thanks. |
If the binding's setter is given an instance of fun subModel msg -> SubModelMsg(subModel, msg) and the update case implemented as | SubModelMsg(subModel, msg) -> { m with SubModel = subModel |> SubModel.update msg |> Some }
You are correct to notice the idempotency involved. There is also commutativity involved. Let Let I actually said "less commutative" though. By less commutative, I mean |
Me too. Mathematically speaking, I think this typically leads to messages that are more commutative. That is definitely the case with I don't have a compelling example to share, but I had something similar to |
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) }
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.) |
All good points above.
In general, commutativity is helpful because it makes things easier to reason about. The intention is that 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.
It is easier to reason about pure code than impure code. 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. |
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:
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 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 🤣
One way it is more complex that you pointed out above is that the log entry for
Suppose 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
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. |
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
Yes, I believe that this can make |
Quick post. First, I figured out how to make the model optional. Just call Second, I was a bit wrong about expressing the more complicated overloads of |
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 Elmish.WPF/src/Elmish.WPF/Binding.fs Lines 534 to 542 in a1f30fa
(Both definitions pass all tests.) |
I figured out the problem for the I forced pushed my branch I believe the problem is that the 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 Now consider this commit. All I did was change the order. Recall the three fundamental concepts currenting combined in our two least completed
I changed the order of those three steps. In the previous commit, the order was 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 One other thing to observe is that when I changed the order, I also had to change one other character. I had to change // 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 Technically, I would say that 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 This naming issue isn't very important right now. I just wanted to point this out given the relevant example. |
Feel free to change the tests anytime.
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 |
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: In our code, there are (at least) four functors involved:
The first one ( The complexity takes a jump when we compose these functors to get make additional functors. (For simplicity, I will just focus on the Sometimes we have a list of bindings (with type // 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, An alternative there is that // 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 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 In addition to 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. |
Thanks for the thorough explanation.
I agree and consider it important that we keep it that way.
Well that looks excellent. Name first, only one module, and only
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. |
Here is a great example of the progress I have made so far. I made "stickiness" into an effect that is composable. Currently in Here is a video that shows the counter value being sticky to even numbers. Try it for yourself in the branch composable/Sticky/example. |
Wonderful! 👏 |
I don't have optional bindings correct yet. My implementation adds the I think I have to implement an optional binding like |
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 |
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.
Yes, I tried some of that but none of it was correct. Maybe I need to use |
The
v4
branch includes the new featuresBindings.mapModel
andBindings.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 thev4
branch).Elmish.WPF/src/Elmish.WPF/Binding.fs
Lines 2299 to 2302 in ff1794e
Elmish.WPF/src/Elmish.WPF/Binding.fs
Lines 2272 to 2276 in ff1794e
Elmish.WPF/src/Elmish.WPF/Binding.fs
Lines 2239 to 2244 in ff1794e
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
andBindings.mapMsgWithModel
. See here and here.The text was updated successfully, but these errors were encountered: