fsprojects-archive / zzarchive-FSharp.Desktop.UI

F# MVC framework for WPF.
http://fsprojects.github.io/FSharp.Desktop.UI/
Other
81 stars 21 forks source link

Separate events instead of discriminated unions? #25

Open cmeeren opened 7 years ago

cmeeren commented 7 years ago

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:

  1. Do you have any comment on this?

  2. Would you say your MVC solution is still relevant, or are there better ways of solving things nowadays?

mattstermiller commented 7 years ago

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.

  1. About combining observables into a single stream. You mentioned wanting to manipulate individual event streams, for example, filtering and throttling. You can do all the setup you want on individual streams in the EventStreams list in the view before they are combined. You probably wouldn't be able to manipulate them after initialization, or at least not easily. I can't think of a good use case for doing so, however, and I would be curious why you might want to. I have never needed to do anything fancy with my event streams, so this has never been a problem for me.

  2. I like this MVC pattern, but this architecture has quirks, especially in F#. My main issue with it is the use of a flat, mutable model and the use of classes. This goes against the grain of functional programming and leads to code with mutation of state that is more difficult to reason about and unit test. My hobby project built on this library, Koffee, has reached about 1600 LOC (not counting tests) and I have become uncomfortable with the architecture. For our work project, I built a fairly simple adaptor around the dispatcher to convert the event handlers to ones that return a new instance of an immutable record model. This makes the controller code easier to reason about and prevents us from doing weird things in event handlers, but it requires the definition of matching mutable class model and record model with the exact same properties. I'm in the process of building an alternative framework that borrows some ideas from this one and from Elm. It will be based on immutable models and simple functions instead of view and controller classes: a function that returns an initial model, one that returns bindings, one that returns event streams, and a dispatcher (sound familiar?). It will be open source, but it will be at least several months before I can get it into a stable state.

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.

cmeeren commented 7 years ago

@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 ListView.ItemSelected event due to a bug with Xamarin.Forms and UWP, but as you suggest, this could be done in the view before the events are combined.

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.

mattstermiller commented 7 years ago

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.

cmeeren commented 7 years ago

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.

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.

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.

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.

cmeeren commented 6 years ago

You said earlier:

For our work project, I built a fairly simple adaptor around the dispatcher to convert the event handlers to ones that return a new instance of an immutable record model. This makes the controller code easier to reason about and prevents us from doing weird things in event handlers, but it requires the definition of matching mutable class model and record model with the exact same properties.

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?

mattstermiller commented 6 years ago

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!

cmeeren commented 6 years ago

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 UsernameTyped of string and PasswordTyped of string actions that fire on every change to the relevant fields, and update the Username and Password in the SignInState part of my root state. This is because these are used (among other things) for determining whether the sign-in button should be enabled (i.e., only if the fields are not empty). This allows me to put all of this logic in the mapper function. I could also do this directly in the view, but it would require me to combine streams of UsernameEntry.TextChanged and PasswordEntry.TextChanged with the VM updates and do the logic in the view, and I'd rather avoid placing logic in the view if possible. And defining some extra state and reducers are so quick and painless in F# that I don't mind keeping some UI state in the store.

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.

mattstermiller commented 6 years ago

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 StateChanging and StateChanged events because it sounds like they may make your code difficult to reason about later on, but without seeing it in action I can't really judge it. You may want to read about modeling impure interactions for functional programming. I am seriously considering this for modeling interactions with the file system in my own app.

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.

cmeeren commented 6 years ago

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 Action DU, I might consider breaking it up later on, but at the moment I don't really see how even a really large number of actions/cases (say, 100 or 200) can cause unavoidable problems, since all of them are basically single-line definitions, and all the functions that take an action (reducers and listeners) match against only those they are interested in and ignore the rest.

StefanBelo commented 6 years ago

I have built this app:

http://bfexplorer.net/

Using FSharp.Desktop.UI, so you can see what can be done.

mattstermiller commented 6 years ago

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?