Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Functional GUIs with F#

2,954 views

Published on

How do you write a GUI app in a functional programming language that prefers immutability? From Visual Basic on we have been taught how to compose interactive UIs using events and mutable properties. Is there any other way? The answer is, yes, indeed there is. Not only can you build UIs using functional concepts, but I will argue that the architecture of such an app is more modular and more robust than the standard architecture resulting from objects sending messages to each other. This talk is an introduction to the fringe world of functional programming using F# and will have information useful to both beginners and practitioners.

Published in: Software
  • Be the first to comment

Functional GUIs with F#

  1. 1. Functional GUIs with F# Frank A. Krueger FRINGE 2015
  2. 2. I ❤️ F#
  3. 3. Simple Domain module DrawingDomain = type Shape = Oval | Rectangle
  4. 4. Simple Domain type Point = { X : float; Y : float } type Size = { Width : float; Height : float } type Frame = { Position : Point Size : Size }
  5. 5. Simple Domain type Shape = | Oval of Frame | Rectangle of Frame
  6. 6. Moar Features! type Shape = | Oval of Frame | Rectangle of Frame | Path of Point list | Union of Shape list | Subtract of Shape list
  7. 7. Common Properties type Element = { Id : Guid Name : string Color : Color Shape : Shape } and Shape = | Oval of Frame | Rectangle of Frame | Path of Point list | Union of Element list | Subtract of Element list
  8. 8. Document Model type Document = { Elements : Element list }
  9. 9. module DrawingDomain = type Color = { R : float; G : float; B : float; A : float } type Point = { X : float; Y : float } type Size = { Width : float; Height : float } type Frame = { Position : Point Size : Size } type Shape = | Oval of Frame | Rectangle of Frame | Path of Point list | Union of Shape list | Subtract of Shape list type Element = { Id : Guid Name : string Color : Color Shape : Shape } type Document = { Elements : Element list }
  10. 10. CRUD Operations
  11. 11. New Document let newDocument = { Elements = [] }
  12. 12. Add Oval let addOval doc frame = let gray = rgb 0.5 0.5 0.5 let oval = { Id = Guid.NewGuid () Name = "Oval" Color = gray Shape = Oval frame } { doc with Elements = oval :: doc.Elements }
  13. 13. Add Oval let d1 = newDocument let d2 = addOval d1 { Position = { X = 0.0; Y = 0.0 } Size = { Width = 200.0; Height = 200.0; } } val d1 : Document = {Elements = [];} val d2 : Document = {Elements = [{Id = 9993a910-c487-4b6f-9025-279ce941518f; Name = "Oval"; Color = {R = 0.5; G = 0.5; B = 0.5; A = 1.0;}; Shape = Oval {Position = {X = 0.0; Y = 0.0;}; Size = {Width = 200.0; Height = 200.0;};};}];}
  14. 14. Remove Elements let removeElement doc id = ...
  15. 15. Mapping val newData : int list = [0; 1000; 2000; 3000; 4000] let data = [0; 1; 2; 3; 4] let times1000 x = x * 1000 let newData = data |> List.map times1000
  16. 16. Map the Document let rec mapElements f elms = let mapChildren e = match e.Shape with | Union children -> { e with Shape = Union (mapElements f children) } | Subtract children -> { e with Shape = Subtract (mapElements f children) } | _ -> e let mapElement = mapChildren >> f elms |> List.choose mapElement and mapDocument f doc = { doc with Elements = mapElements f doc.Elements }
  17. 17. Remove Element et removeElement doc id = let keep e = if e.Id = id then None else Some e mapDocument keep doc
  18. 18. Change the Color let setColor doc ids newColor = let selected e = Set.contains e.Id ids let set e = if selected e then Some { e with Color = newColor } else Some e mapDocument set doc
  19. 19. Mutant Free Zone
  20. 20. GUIs
  21. 21. OOP GUIs FrankName Code Behind, View Model, View Controllers, Reactive, Binding, whatever… 1. User types something 2. Text property changes 3. Some middleman to help you sleep at night 4. Mutate the Model (change properties) Model
  22. 22. Mutant Love Zone
  23. 23. F# is OOP type ShapeMutant () = let mutable color = rgb 0.5 0.5 0.5 member this.Color with get () = color and set v = color <- v
  24. 24. F# is OOP type ShapeMutant () = let mutable color = rgb 0.5 0.5 0.5 member this.Color with get () = color and set v = color <- v
  25. 25. How do I keep my functional model and still use OOP UI libraries?
  26. 26. Answer • Views reference a way to find the data they need, they don’t reference the data directly • There is a single Update function on the Document that: 1. Sets a new document state 2. Signals that the document has changed
  27. 27. OOP Name Editorpublic class ElementNameEditor { TextEdit view; Element model; void Initialize () { // Display the current data view.Text = model.Name; // Handle user changes view.TextChanged += (s, e) => model.Name = view.Text; // Refresh when the doc changes model.PropertyChanged += (s, e) => { if (e.Name == "Text") view.Text = model.Name; } ; } }
  28. 28. public class ElementNameEditor { TextEdit view; Element model; void Initialize () { // Display the current data view.Text = model.Name; // Handle user changes view.TextChanged += (s, e) => model.Name = view.Text; // Refresh when the doc changes model.PropertyChanged += (s, e) => { if (e.Name == "Text") view.Text = model.Name; } ; } } OOP Name Editor Direct Reference MAGIC
  29. 29. Two Changes…
  30. 30. 1. Instead of a Direct Reference, use an Indirect Reference
  31. 31. 2. Instead of MAGIC, call a function and handle the update event
  32. 32. Functional Name Editor type ElementNameEditor (view : TextEdit, docc : DocumentController, id) = member this.Initialize () = // Handle user changes view.TextChanged.Add (fun _ -> let newName = view.Text let setName e = if e.Id = id then Some { e with Name = newName } else Some e let newDoc = mapDocument setName docc.Data docc.Update newDoc) // Display the current data let refresh doc = let model = getElement id doc view.Text <- model.Name refresh docc.Data // Refresh when the doc changes docc.Updated.Add refresh
  33. 33. Functional Name Editor type ElementNameEditor (view : TextEdit, docc : DocumentController, id) = member this.Initialize () = // Handle user changes view.TextChanged.Add (fun _ -> let newName = view.Text let setName e = if e.Id = id then Some { e with Name = newName } else Some e let newDoc = mapDocument setName docc.Data docc.Update newDoc) // Display the current data let refresh doc = let model = getElement id doc view.Text <- model.Name refresh docc.Data // Refresh when the doc changes docc.Updated.Add refresh Indirect Reference
  34. 34. Functional Name Editor type ElementNameEditor (view : TextEdit, docc : DocumentController, id) = member this.Initialize () = // Handle user changes view.TextChanged.Add (fun _ -> let newName = view.Text let setName e = if e.Id = id then Some { e with Name = newName } else Some e let newDoc = mapDocument setName docc.Data docc.Update newDoc) // Display the current data let refresh doc = let model = getElement id doc view.Text <- model.Name refresh docc.Data // Refresh when the doc changes docc.Updated.Add refresh Explicit Update Function
  35. 35. Functional Name Editor type ElementNameEditor (view : TextEdit, docc : DocumentController, id) = member this.Initialize () = // Handle user changes view.TextChanged.Add (fun _ -> let newName = view.Text let setName e = if e.Id = id then Some { e with Name = newName } else Some e let newDoc = mapDocument setName docc.Data docc.Update newDoc) // Display the current data let refresh doc = let model = getElement id doc view.Text <- model.Name refresh docc.Data // Refresh when the doc changes docc.Updated.Add refresh Single Update Event
  36. 36. DocumentController type DocumentController () = let updated = Event<_> () let mutable data = newDocument member this.Data = data member this.Update newData = data <- newData updated.Trigger newData member this.Updated = updated.Publish
  37. 37. DocumentController type DocumentController () = let updated = Event<_> () let mutable data = newDocument member this.Data = data member this.Update newData = data <- newData updated.Trigger newData member this.Updated = updated.Publish Single Source of Mutation
  38. 38. Why? public class ElementNameEditor { TextEdit view; Element model; void Initialize () { // Display the current data view.Text = model.Name; // Handle user changes view.TextChanged += (s, e) => model.Name = view.Text; // Refresh when the doc changes model.PropertyChanged += (s, e) => { if (e.Name == "Text") view.Text = model.Name; } ; } } type ElementNameEditor (view : TextEdit, docc : DocumentController, id member this.Initialize () = // Handle user changes view.TextChanged.Add (fun _ -> let newName = view.Text let setName e = if e.Id = id then Some { e with Name = newName } else Some e let newDoc = mapDocument setName docc.Data docc.Update newDoc) // Display the current data let refresh doc = let model = getElement id doc view.Text <- model.Name refresh docc.Data // Refresh when the doc changes docc.Updated.Add refresh
  39. 39. Five Reasons 1. Undo, Redo, and History 2. Atomic Save and Background Open 3. Background Updates 4. No Dangling References 5. Keep My Functional Model
  40. 40. Document-based App Requirements • Undo & Redo • Background Save & Open
  41. 41. OOP Undo Architecture Whenever you change the document, pass a reference to another function that can undo all those changes
  42. 42. OOP Undo view.TextChanged += (s, e) => SetName (view.Text); void SetName (string newName) { var oldName = model.Name; UndoManager.RegisterUndo (() => SetName (oldName)); UndoManager.SetAction ("Rename"); model.Name = newName; } view.TextChanged += (s, e) => model.Name = view.Text;
  43. 43. For every command you write, you have to write its inverse 😳
  44. 44. F# Undo docc.Update newDoc
  45. 45. F# Undo docc.Update newDoc docc.Update newDoc "Rename"
  46. 46. F# Undo docc.Update newDoc docc.Update newDoc "Rename" No Need to Write an Inverse Function!
  47. 47. type DocumentControllerWithUndo () = let updated = Event<_> () let mutable historyIndex = 0 let mutable history = [(newDocument, "New")] member this.Data = history.[historyIndex] member this.Update newData message = history <- (newData, message) :: history historyIndex <- 0 updated.Trigger newData member this.Updated = updated.Publish member this.Undo () = historyIndex <- historyIndex + 1 updated.Trigger this.Data member this.Redo () = historyIndex <- historyIndex - 1 updated.Trigger this.Data
  48. 48. type DocumentControllerWithUndo () = let updated = Event<_> () let mutable historyIndex = 0 let mutable history = [(newDocument, "New")] member this.Data = history.[historyIndex] member this.Update newData message = history <- (newData, message) :: history historyIndex <- 0 updated.Trigger newData member this.Updated = updated.Publish member this.Undo () = historyIndex <- historyIndex + 1 updated.Trigger this.Data member this.Redo () = historyIndex <- historyIndex - 1 updated.Trigger this.Data
  49. 49. Atomic Save Because the DocumentController has a immutable snapshots of the data, atomic saves are trivial.
  50. 50. Atomic Save Because the DocumentController has a immutable snapshots of the data, atomic saves are trivial.
  51. 51. Background Open Because the Document is immutable, it doesn’t matter what thread it came from.
  52. 52. Background Open Because the Document is immutable, it doesn’t matter what thread it came from.
  53. 53. Background Updates Because the Document is immutable, it doesn’t matter what thread was used to create it or how long it took to generate.
  54. 54. Background Updates Because the Document is immutable, it doesn’t matter what thread was used to create it or how long it took to generate.
  55. 55. No Dangling References Most of my memory leaks occur because my model has an event that has a reference to a UI object. I never remember to unsubscribe all my events. With fewer events, there are fewer mistakes to be made, and its easier to track down trouble makers.
  56. 56. No Dangling References Most of my memory leaks occur because my model has an event that has a reference to a UI object. I never remember to unsubscribe all my events. With fewer events, there are fewer mistakes to be made, and its easier to track down trouble makers.
  57. 57. For This… 1. Undo, Redo, and History 2. Atomic Save and Background Open 3. Background Updates 4. No Dangling References 5. Keep My Functional Model I Get… 1. Instead of a Direct Reference, use an Indirect Reference 2. Call a single Update function and handle the Updated event
  58. 58. Thank You Frank A. Krueger @praeclarum http://github.com/praeclarum http://praeclarum.org fak@praeclarum.org

×