Skip to content

Instantly share code, notes, and snippets.

@applejag
Last active November 1, 2020 21:45
Show Gist options
  • Save applejag/87d65b8b621b3193c94c79ae08c251ba to your computer and use it in GitHub Desktop.
Save applejag/87d65b8b621b3193c94c79ae08c251ba to your computer and use it in GitHub Desktop.

This is mostly a RnD idea that came up when looking into mattstermiller/koffee#20

What I try here is to extract away the different prompts into submodules to be able to assign to the MainModel what dispatcher to use.

What I tried

  • Creating WPF UserControl's. Sadly I couldn't find an easy way as FsXaml doesn't support proper code-behind for controls. Also Acadian-Ambulance/VinylUI doesn't seem to have the greatest support for handling that kind of stuff. Would be nice to have though, like to let VinylUI to handle the dispatcher and focus stacks.

  • Create IPrompt interface with dispatcher method that different prompts (ex: DeleteBookmarkPrompt vs GoToBookmarkPrompt) implements.

  • Change to module based with static dispatcher functions. That's the code snippet I provided here with Prompts.fs.

Next steps

  • Edit the model binding in MainView.fs and try in an abstract way display the correct data depending on selected prompt. Would be nice to allow specifying if we want to show a list and/or the input field, and what text should be written in those field.

  • Make composing of prompts better (in the code, that is). The current module based approach makes it tedious to handle that stuff and the file could get really big. It isn't that easy to read actually, in comparison to the previous solution.

  • Make some unified way of opening prompts. Get's annoying quite quick though as you can't really have circle dependencies in F# in a smooth way. For example: I can't access Koffee.MainLogic.Nav from the prompts without doing some major refactoring. I can't create a IPrompt interface that the MainModel has as a property/member for routing dispatcher events to the hypothetical "selected prompt instance", as some of the functions has dependency on the MainModel or MainEvents itself (ex: the dispatcher).

  • Change the flow of the fallback dispatcher calls. Currently they go in kind of reverse order. Optimally we want the ClosablePrompt dispatcher to fire first, then if it didn't handle the event it should continue to the next one.

Reflection

I think this is me bringing too much C#/OO into F#. I want to solve this problem with polymorphism, but I can feel the framework and language is somewhat holding me back. The fact that everything has to be declared above/before does force me to not cheat and create a spaghetti of dependencies for the prompts, and that's probably for the better as it's probably considered bad-practice in F# code. But what do I know.

It's of course possible to create FSM's for this case. I just haven't found the way to do so yet in a clean way, while still staying true with the conventions of VinylUI and F#.

Bottom line: I think this is trying to bend the language and framework against their wills. I am consceding this fight and going with basic implementation instead.

module Koffee.Prompts
open VinylUI
type ModifierKeys = System.Windows.Input.ModifierKeys
type Key = System.Windows.Input.Key
module ClosablePrompt =
let closePrompt handleEvt (model: MainModel) =
handleEvt()
model.WithoutPrompt()
let isClosePromptChord chord =
chord = (ModifierKeys.None, Key.Escape)
let (|ClosePromptKeyPress|_|) (evt: MainEvents) =
match evt with
| KeyPress (chord, handler) when isClosePromptChord chord -> Some handler
| _ -> None
let dispatcher fallbackDisp evt =
match evt with
| ClosePromptKeyPress handler -> Sync (closePrompt handler.Handle)
| evt -> fallbackDisp evt
module DeleteBookmarkPrompt =
let deleteBookmark char handleEvt model = asyncSeq {
handleEvt()
match model.Config.GetBookmark char with
| Some path ->
yield
{ model with
Config = model.Config.WithoutBookmark char
Status = Some <| MainStatus.deletedBookmark char (path.Format model.PathFormat)
}
| None ->
yield { model with Status = Some <| MainStatus.noBookmark char }
}
let dispatcher fallback evt =
match evt with
| InputCharTyped (c, handler) -> Async (deleteBookmark c handler.Handle)
| _ -> ClosablePrompt.dispatcher fallback evt
module BaseBookmarkPrompt =
let switchToDeleteBookmarkPrompt handleEvt model =
handleEvt()
{ model with PromptDispatcher = Some DeleteBookmarkPrompt.dispatcher }
let dispatcher fallback evt =
match evt with
| InputDelete handler -> Sync (switchToDeleteBookmarkPrompt handler.Handle)
| _ -> ClosablePrompt.dispatcher fallback evt
module GoToBookmarkPrompt =
let goToBookmark char handleEvt model = asyncSeq {
handleEvt()
match model.Config.GetBookmark char with
| Some path ->
yield model
// Oh crap, I need access to Koffee.MainLogic.Nav... Refactoring out Nav from there doesnt look trivial.
//yield! Nav.openPath fs path SelectNone model
| None ->
yield { model with Status = Some <| MainStatus.noBookmark char }
}
let dispatcher fallback evt =
match evt with
| InputCharTyped (c, handler) -> Async (goToBookmark c handler.Handle)
| _ -> BaseBookmarkPrompt.dispatcher fallback evt
module SetBookmarkPrompt =
let withBookmark char model =
{ model with
Config = model.Config.WithBookmark char model.Location
Status = Some <| MainStatus.setBookmark char model.LocationFormatted
}
let setBookmark char model = asyncSeq {
match model.Config.GetBookmark char with
| Some existingPath ->
yield
{ model with
InputMode = Some (Confirm (OverwriteBookmark (char, existingPath)))
InputText = ""
}
| None ->
yield withBookmark char model
}
let dispatcher fallback evt =
match evt with
| InputCharTyped (c, _) -> Async (setBookmark c)
| _ -> BaseBookmarkPrompt.dispatcher fallback evt
@applejag
Copy link
Author

Don't know how to make a dependency/inheritance chain but where the root types implementation is called first.

Hmm that's quite bad in theory. Goes against the open-close-principle. Much better to try doing it with classes then and calling base.dispatcher, or do it with modules as shown in the snippet above.
Personally prefer doing it with classes. Is that bad practice in F#? I hope one day I'll know.. :)

@mattstermiller
Copy link

It's true VinylUI wasn't designed with composability in mind to break up logic for a single form/window, however, I (think) it should still be possible by creating sub-models and sub-dispatchers of sorts. It would probably be clunky, but I haven't tried it.

This looks like you're trying to use the Strategy pattern but with functions of the same name in different modules? It's normally implemented with interface implementations or maybe a switch (which is basically what Koffee has now).

I don't really understand the problem you're trying to solve. Is it that the code to handle the different prompts is in three separate pattern matches and you're trying to bring them all together? If that's the case, an interface and different implementations could be used. The interface would define the 3 functions needed and there would be a function that, given a Prompt value, would return the correct implementation.

The binding is admittedly gross. The problem is that in order to react correctly for prompts it needs so many individual pieces of data. The way bindings in VinylUI work is that you can't access model data in a binding without it being part of the binding. This helps to prevent problems with the UI not being updated when something it depends on changes. Because the input mode is tied to all these different behaviors, this one binding has to pull in so much. It could be broken up so that there's multiple bindings on input mode, but each for a specific mode that is bound to the different properties that mode needs, but I'm not sure that would turn out better. I'm not sure what a better solution would look like.

@mattstermiller
Copy link

The fact that everything has to be declared above/before does force me to not cheat and create a spaghetti of dependencies for the prompts, and that's probably for the better as it's probably considered bad-practice in F# code.

You are correct. F# was intentionally designed so that you can't refer to things defined later in a file (except with the and keyword), or defined in files lower in the project. This prevents circular dependencies within projects. This is jarring coming from C#/Java, but I believe that once you get used to it, it really does produce better architecture. The main function in an F# project is always in the bottom file, and the utilities and global models are in the top files. You are forced to think about how different parts of the program should interact in a clean way. It forces you to separate the low-level "services" or whatever you want to call them from your higher-level business logic and makes it difficult to create spaghetti architecture. After 4 years of experience with F#, I'm a firm believer that this "limitation" of the compiler is actually a solid language decision.

@applejag
Copy link
Author

applejag commented Nov 1, 2020

The problem I was trying to solve is close to the one you guessed at. I was trying to abstract away the prompt to have for example a prompt class that handles how the input is handled and data is diplayed, and change the binding to just bind to the given implementation's values. It was also an effort as I tried to use the same for the history panel "prompt". Using modules instead of interfaces was an experiment to see if that was nicer, but it turned a bit into unclear code. Classes are obviously the way to go when it comes to inheritence, but I tried with modules anyway. I blame my tired mind at the time for that approach.

It's normally implemented with interface implementations or maybe a switch

That's how I'm used to do it in C#, but to reinforce my statement: I didn't know if those patterns can be taken into F#. I haven't studied any functional patterns yet.

The multibinding reminds me of old AngularJs code :) It's a little bit nostalgic (if you're unfamiliar with that, here's the docs on that. The dependency injection gets nasty quick in the same way, where you have to specify everything twice). That is, if you're referring to the Bind.modelMulti for the InputMode. That section I think could be shortened quite a bit if all those parameters were moved to model properties, and then have those properties be updated by some IPrompt implementation. For reference, I'm trying to follow Uncle Bob's G23: Prefer Polymorphism to If/Else or Switch/Case heuristic here.

Your comments there on F# are so on point. I can really see how those limitations result in better code, but yes this does feel veeery jarring at the moment. So many habits I have to throw out the window that I don't know which ones to keep. I don't know if it's F# or you, but Koffee is actually very easy and fun to write tests for (except the model binding). The "pure"-ness and lack of objects and stateful methods is actually nice. Either way, big kudos to you for that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment