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.
7. Moar Features!
type Shape =
| Oval of Frame
| Rectangle of Frame
| Path of Point list
| Union of Shape list
| Subtract of Shape list
8. 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
10. 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
}
16. 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
17. 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
}
19. 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
22. 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
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. 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
26. How do I keep my functional
model and still use OOP UI
libraries?
27. 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
28. 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;
} ;
}
}
29. 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
31. 1. Instead of a Direct
Reference, use an Indirect
Reference
32. 2. Instead of MAGIC, call a
function and handle the
update event
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
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
Indirect Reference
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
Explicit Update Function
36. 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
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
38. 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
39. 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
40. 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
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. 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.
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. 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.
58. 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
59. Thank You
Frank A. Krueger
@praeclarum
http://github.com/praeclarum
http://praeclarum.org
fak@praeclarum.org
Editor's Notes
Primary amongst these is its succinct syntax for modeling the domain of your app.
For a working example through this presentation, I want to discuss a drawing program. Drawing programs let you:
draw shapes
combine shapes
edit the hierarchy
edit the graphics themselves
property editors
If you're new to F#, then this reads simply as "Shapes are Ovals OR Rectangles". This is called a discriminated union. Enums are very simple discriminated unions.
We need to allow the user to position these shapes. We do this by adding data to the variants - in this case a frame. Let's start by defining the frame.
These curly braces denote "records" - objects where all the properties are public. This is safe in F# because everything is immutable by default so no one can mess with your objects.
We then add this frame data to the various shapes.
That's pretty good, but let's also make this an advanced drawing program with boolean operations on the shapes.
Note that ships are now recursive
All shapes have a few common properties. We will pull these common things out into an Element record.
Now we need to write all the operations that the user can perform on the document. I’ll show just a few to give you a flavor.
Takes a document and a frame, and gives a NEW DOCUMENT with the oval added to the beginning of the list of elements
Takes a document and a frame, and gives a NEW DOCUMENT with the oval added to the beginning of the list of elements
This is an important function, but it’s also a great opportunity to write a new kind of helper function
Mapping is just applying a function to a bunch of data. In this case we apply a function that multiplies a number by 1000.
But you can map a lot more than just normal lists. You can map complicated data like documents.
We never changed the document. We just kept on creating new documents with the new data. For efficiency we also reused objects from the last document.
GUIs are tangled webs of state and events. This goes against the simplicity of my Model.
Since GUI libraries work on the premise of mutating state, they force that concept onto your model.
This is fine because F# is an object oriented language
But oh. This is so not F#. How do we get out of this mutant love zone?
Make this compatible with my Functional Model.
Am I just holding onto an ideology?
I love to write document-based apps.
That’s a lot of code just for a property set, imagine a complicated function!
Now every Update is undoable. We even have a full history so we can be advanced like Photoshop.