functional guis with f#

59
Functional GUIs with F# Frank A. Krueger FRINGE 2015

Upload: frank-krueger

Post on 12-Aug-2015

181 views

Category:

Software


1 download

TRANSCRIPT

  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 { if (e.Name == "Text") view.Text = model.Name; } ; } }
  24. 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
  25. 29. Two Changes
  26. 30. 1. Instead of a Direct Reference, use an Indirect Reference
  27. 31. 2. Instead of MAGIC, call a function and handle the update event
  28. 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 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 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 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 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 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;
  29. 43. For every command you write, you have to write its inverse
  30. 44. F# Undo docc.Update newDoc
  31. 45. F# Undo docc.Update newDoc docc.Update newDoc "Rename"
  32. 46. F# Undo docc.Update newDoc docc.Update newDoc "Rename" No Need to Write an Inverse Function!
  33. 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