-
Notifications
You must be signed in to change notification settings - Fork 21
Separate events instead of discriminated unions? #25
Comments
Hi cmeeren, This project appears to be abandoned. I have been using this library for almost a year both at work and on a hobby project, and my colleagues and I have been unable to contact the maintainers. I happened to come across your questions, so I will try to answer them as best I can.
Unfortunately, there doesn't seem to be many examples of people building desktop apps with F#. Many who are doing so can't share their code because it belongs to their employer. You may want to look into the Gjallarhorn library which could help you build an Elm-style event-driven app. You could talk to Reed Copsey and a few other desktop devs on the F# slack channel (you have to join the F# foundation to get an invite to their slack). I know getting started in this area in F# in difficult, so if you have further questions, I'd be happy to try answering them. Just PM me. |
@mattstermiller Thanks! I appreciate your taking the time to reply to my questions. I don't really have any examples of use-cases that would require different subscribers to filter different events in different ways, I just couldn't think of any good reasons why this should be hard to do, or a bad idea. As an example of filtering in general, I need to throttle the Regarding 2. and general comments on desktop apps with F#: The view is stateful and mutable by nature. When working on platforms with data bindings, doesn't this kind of architecture make sense? (Specifically, I'm talking about a mutable data-only (view) model bound to the view and exposing event stream(s), with a controller subscribing to the VM events and updating the VM.) For one, the ("physical") view is mutable by nature, so having a mutable view model as a logical model of the view to program against seems to make sense. Furthermore, if one wanted to have an immutable view model and re-bind the view to that all the time, that would significantly impact performance AFAIK, so the alternative (as far as I can see) would be to directly update the view and bypass the view model altogether, which somewhat tightly couples internal stuff with the view platform. I know mutability is "against the grain", but 1) F# is functional first, not functional only, 2) mutability doesn't seem to me to be a decidedly bad idea in all cases as long as you use it sparingly and in a controlled manner, and 3) the edges of applications are where side effects (e.g. mutation) are located anyway. In short, it seems to me that having a simple mutable "view model" (only state, no behaviour) is a simple way to use F# with e.g. Xamarin, UWP, etc. Then again, I have (as mentioned) just recently started learning F#, and have yet to use it for a real-world project. |
You make good points. One of the strengths of F# in my opinion is its flexibility to be used in multiple paradigms. I don't think there is anything truly wrong with the architecture that this library lays out, and MVC is a nice pattern. However, I've gotten a taste of more pure functional programming, so working with state in this MVC pattern feels gross to me now. I don't consider the controller logic as the edge of my applications as there is considerable amounts of code there, so I want it to be solid. I feel like there must be a better way. I want an abstraction that allows me to use functional paradigms to express UI behavior and I believe the library I am building will give me just that. The way I am handling the binding with immutable models is slightly magical, I admit; For each binding created, there is a mutable proxy class created behind the scenes that is bound to the view. When the proxy changes, the framework uses reflection to create a new model record, and when event handlers return a model with new values, the framework detects the changes and updates the bound proxies. Is this "slow"? It will definitely be slower than directly using a mutable model. Is it slow enough that users will notice? I highly doubt it. Besides, in our application at work, latency to the database is greater than anything else we do. There might be some cost to this abstraction, but I think it will be cheap enough, and the benefit in development experience far outweighs it. It will allow me to write code that is simpler, easier to understand, testable, and reliable. Even if performance does become a concern, I know of a few tricks that could speed up the things I am currently using reflection for, but I'm not convinced I will even need to optimize it. Either way, be wary of using this particular library because it is not being maintained and it has rough edges and missing features, particularly in data binding. You can fill in the gaps if you want, as I have been doing, but it is work. What other alternatives are there? I don't know. I've had some discussions on the F# slack, but nobody seems to have a clear answer. Reed Copsey talks about how builds apps that are purely event driven, so every time the user changes FirstName.Text, that fires an event that he writes a handler for FirstNameChanged which he handles by setting a property on a model or permuting a record with that new value, for example. That seems like a lot of work to me though, so I'm building my own framework. It is my hope that it is successful and I will be able to share it, with examples, with other devs in the future as a well-documented option for building desktop apps. |
Sounds like what I did recently with Redux.NET. It certainly was a lot of work, and after having to add a few new views to the app I quickly went back to "plain old" MVVM. A kind of in-between would also have worked better, I think.
Well-documented being the keyword here, I think. I've seen a few ideas/libraries on how to do UI apps in F#, but most seem to be either unrefined ideas or poorly documented libraries. |
You said earlier:
Why do data binding at all? Why not simply have the view update itself directly based on the immutable record model? That's what I'm considering now, but nobody else seems to - everyone seems to use data binding as the last step to actually perform the physical view update. I see no reason why the codebehind can't simply mechanically update the view based on the same record that would otherwise have to be mapped to a bindable view model. I also can't see the view's events being much of a showstopper in this regard. Am I missing anything? |
Sure, there's no reason you can't update the view directly from the model. You would also need to permute the record model based on changes to the view. This would be annoying boilerplate code to write with event handlers unless you create helpers that use reflection or such. What you might lose is other features of databinding, which includes whether the binding should trigger as soon as the control is changed or wait until it is validated. I think WPF has other data binding features you might care about. I can't think of any huge reason not to do it, but the best way to find the weaknesses is to just try it! |
I'm using Xamarin.Forms, which has no built-in validation, so that's not relevant to me. What I'm going for now, is back to a kind of Redux architecture, which seems to require far less boilerplate in F# than in C# (what a surprise, huh). Specifically (passing familiarity with Redux assumed):
Using this method, I have opted for keeping more or less all state - including UI state - in the Store. For example, I have What do you think, in general? To me this makes perfect sense. I tried it last night and was able to get a simple sign-in screen working perfectly. |
I'm not sure I have a complete picture in my head of your architecture just from this description, but it sounds like you are on the right track to something with good separation of concerns. I'm not totally sold on the What really concerns me is "A Store class contains the app's entire state" and "All actions in the app are cases on a single DU". Unless you are doing something clever with nesting, it sounds like these would grow to unmanageable sizes for anything other than a toy app. Also, consider how unit tests might have to be written against any parts of your code that contain logic; if you would have to do lots of setup or do complicated things to write a meaningful test, it may be a sign that some refactoring is in order. Even if you never write tests, considering the testability of your logic code can be a good indicator of separation of concerns. Maybe you can make it work, but you might want to start thinking about how this can be divided and/or composed as necessary. But again, without seeing how this actually works in your app, it is difficult for me to say whether this will become a problem. My general advice is to start developing a real app with whatever design seems to work and adapt as you find new requirements. Even if there's something about it that feels weird right now, in my experience it is better to continue building until you really experience the pain points and can define exactly what needs to change and an idea of what it should be changed to. It is good to spend some time considering your architecture at the beginning, but once you start developing against it, it is counter-productive to continuously question and redesign the architecture before getting anywhere in your app's development. It is better to set milestones and only when you complete one, re-evaluate designs and do refactoring. This has worked very well for me in my hobby project Koffee. I have pushed forward with using this library even though I don't like parts of the resulting design, but now I have a fairly usable app and I know some things that should change and am currently performing a major refactor phase. It mostly boils down to separating my controller logic from dependencies better and using an immutable model in the controller logic. I am moving all of the real controller logic a.k.a. event handlers into functions in a separate module, injecting only the dependencies I need for each handler via partial application. This is making my tests much easier to setup and reason about since I no longer need mocks and I broke my handler chaining in the tests by making the chained handler be a dependency (parameter). You can see what I'm doing if you look at the refactor branch in my repo. |
Thanks for helpful feedback, particularly regarding sticking with your architecture choice until you really see where it's going. Your concerns seem to be mostly related to the Redux architecture. If you're not at least slightly familiar with it, then my description would have been woefully inadequate. Rest assured, much wiser heads than mine have come up with this. You are right in that the state (and the reducers) are usually nested. (The structure of the state in Redux architecture is an important question that gets asked a lot and for which there seems to be no clear answer.) As for the |
I have built this app: Using FSharp.Desktop.UI, so you can see what can be done. |
Hi @StefanBelo, that is some impressive-looking software. How do you feel about the development and maintenance experience using this framework compared to other things? |
I know this is an old library by now, but it still appeared high up on the search results, so here I am.
I'm learning F# (so all of the below may be based on misunderstandings) and I want to use F# for a Xamarin app. Your MVC way of thinking as described in your series seems to have a lot of merit. However, I'm a bit fuzzy on why you have a single event stream per MVC-combo where all the different events are separate cases of a single discriminated union. As far as I can see, this precludes easy Rx manipulation of the different events.
For example, say you have two events for a view: MouseMoved and ButtonClicked. According to your architecture, these would then be two cases of a discriminated union. But if you want to e.g. throttle MouseMoved, or filter it based on coordinates, I can't see an elegant way of doing this. If the two events were instead two different IObservables, this would be trivial.
My two questions:
Do you have any comment on this?
Would you say your MVC solution is still relevant, or are there better ways of solving things nowadays?
The text was updated successfully, but these errors were encountered: