F# Implementation of The Elm Architecture - Part 2

This is the second part of a prototype of The Elm Architecture in F#. The first post covered the logical UI and this covers using it with WPF and Xamarin.

Native UI Implementation

Below is the WPF implementation of the INativeUI interface. The Xamarin implementation is very similar.

Threading proves to be simple with all the updates being performed on the UI thread in a single call. UIUpdate maps well to the operations required to locate and update the native UI elements.

module WPF =
    let CreateNaiveUI (root:ContentControl) =
        
        let rec createUI ui : UIElement =
            match ui with
            |Text text ->
                let c = Label(Content=string text)
                upcast c
            |Input (text,event) ->
                let c = TextBox(Text=string text)
                let event = !event
                c.TextChanged.Add(fun _ -> let t = c.Text
                                           async { !event t } |> Async.Start)
                upcast c
            |Button (text,event) ->
                let c = Button(Content=string text)
                let event = !event
                c.Click.Add(fun _ -> async { (!event)() } |> Async.Start)
                upcast c
            |Div (layout,list) ->
                let children = List.map createUI list
                let c = StackPanel(Orientation=
                                    match layout with 
                                    |Vertical->Orientation.Vertical
                                    |Horizontal->Orientation.Horizontal)
                List.iter (c.Children.Add>>ignore) children
                upcast c

        let rec locatePanel loc : Panel =
            match loc with
            |[] -> root.Content :?> _
            |i::xs -> (locatePanel xs).Children.Item i :?> _

        let uiUpdate u =
            match u with
            | InsertUI (loc,ui) ->
                match loc with
                |[] -> root.Content <- createUI ui
                |i::xs -> (locatePanel xs).Children.Insert(i,createUI ui)
            | UpdateUI (loc,ui) ->
                let element = match loc with
                              |[] -> root.Content :?> _
                              |i::xs -> (locatePanel xs).Children.Item i
                match ui with
                | Text text -> (element :?> Label).Content <- string text
                | Input (text,_) -> (element :?> TextBox).Text <- string text
                | Button (text,_) -> (element :?> Button).Content <- string text
                | Div _ -> ()
            | ReplaceUI (loc,ui) ->
                match loc with
                |[] -> root.Content <- createUI ui
                |i::xs ->
                    let c = (locatePanel xs).Children
                    c.RemoveAt i
                    c.Insert(i,createUI ui)
            | RemoveUI loc ->
                match loc with
                |[] -> ()
                |i::xs -> (locatePanel xs).Children.RemoveAt i
            | EventUI _ -> ()

        { new INativeUI with
            member __.Send list =
                root.Dispatcher.Invoke (fun () -> List.iter uiUpdate list)
        }

Results

The same CounterList UI application from the previous post has been used across a number of native UIs. The example source produces the following mobile and desktop UIs.

Conclusion

The Elm Architecture continues to look to be a very promising pattern.

The INativeUI implementation is a single place for native UI element creation and is much more DRY than other UI models. So far, styling has not been considered, but this single place should make it easier with both an Elm and a CSS model being possible.

The Elm Architecture moves the view and event logic away from the native UI. This has the benefit of making the UI more testable and at the same time making migration of the native UI easier.

These benefits, combined with the type safety and composability outlined in the previous post, make this pattern compelling.

UPDATED:

Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
    interface IReadOnlyList<'T>
    interface IReadOnlyCollection<'T>
    interface IEnumerable
    interface IEnumerable<'T>
    member GetSlice : startIndex:int option * endIndex:int option -> 'T list
    member Head : 'T
    member IsEmpty : bool
    member Item : index:int -> 'T with get
    member Length : int
    member Tail : 'T list
    ...
val remove : n:int -> l:'a list -> 'a list * 'a list
val n : int
val l : 'a list
val pop : (int -> 'b list -> 'b list -> 'b list * 'b list)
val l : 'b list
val p : 'b list
val x : 'b
val xs : 'b list
val add : p:'a list -> l:'a list -> 'a list
val p : 'a list
val x : 'a
val xs : 'a list
val removei : i:int -> l:'a list -> 'a list
val i : int
val t : 'a list
val tail : list:'T list -> 'T list
val replacei : i:int -> item:'a -> l:'a list -> 'a list
val item : 'a
val revMap : mapping:('a -> 'b) -> list:'a list -> 'b list
val mapping : ('a -> 'b)
Multiple items
val list : 'a list

--------------------
type 'T list = List<'T>
val work : ('a list -> 'b list -> 'b list)
val input : 'a list
val output : 'b list
val mapAt : i:int -> mapping:('a -> 'a) -> list:'a list -> 'a list
val mapping : ('a -> 'a)
val removed : 'a list
val tail : 'a list
val head : list:'T list -> 'T
Multiple items
module Event

from Microsoft.FSharp.Control

--------------------
type 'msg Event = ('msg -> unit) ref ref


 Message event used on the primative UI components.


--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
  new : unit -> Event<'Delegate,'Args>
  member Trigger : sender:obj * args:'Args -> unit
  member Publish : IEvent<'Delegate,'Args>

--------------------
new : unit -> Event<'Delegate,'Args>
type unit = Unit
Multiple items
val ref : value:'T -> 'T ref

--------------------
type 'T ref = Ref<'T>
type Layout =
  | Horizontal
  | Vertical


 Layout for a section of UI components.
union case Layout.Horizontal: Layout
union case Layout.Vertical: Layout
type UI =
  | Text of string
  | Input of string * string Event
  | Button of string * unit Event
  | Div of Layout * UI list


 Primative UI components.
union case UI.Text: string -> UI
Multiple items
val string : value:'T -> string

--------------------
type string = System.String
union case UI.Input: string * string Event -> UI
union case UI.Button: string * unit Event -> UI
union case UI.Div: Layout * UI list -> UI
type 'T list = List<'T>
type UIUpdate =
  | InsertUI of int list * UI
  | UpdateUI of int list * UI
  | ReplaceUI of int list * UI
  | RemoveUI of int list
  | EventUI of (unit -> unit)


 UI component update and event redirection.
union case UIUpdate.InsertUI: int list * UI -> UIUpdate
Multiple items
val int : value:'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
union case UIUpdate.UpdateUI: int list * UI -> UIUpdate
union case UIUpdate.ReplaceUI: int list * UI -> UIUpdate
union case UIUpdate.RemoveUI: int list -> UIUpdate
union case UIUpdate.EventUI: (unit -> unit) -> UIUpdate
Multiple items
type UI =
  | Text of string
  | Input of string * string Event
  | Button of string * unit Event
  | Div of Layout * UI list


 Primative UI components.


--------------------
type 'msg UI =
  {UI: UI;
   mutable Event: 'msg -> unit;}


 UI component including a message event.
Multiple items
UI.UI: UI

--------------------
type UI =
  | Text of string
  | Input of string * string Event
  | Button of string * unit Event
  | Div of Layout * UI list


 Primative UI components.


--------------------
type 'msg UI =
  {UI: UI;
   mutable Event: 'msg -> unit;}


 UI component including a message event.
Multiple items
UI.Event: 'msg -> unit

--------------------
module Event

from Microsoft.FSharp.Control

--------------------
type 'msg Event = ('msg -> unit) ref ref


 Message event used on the primative UI components.


--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
  new : unit -> Event<'Delegate,'Args>
  member Trigger : sender:obj * args:'Args -> unit
  member Publish : IEvent<'Delegate,'Args>

--------------------
new : unit -> Event<'Delegate,'Args>
type App<'msg,'model> =
  {Model: 'model;
   Update: 'msg -> 'model -> 'model;
   View: 'model -> 'msg UI;}


 UI application.
App.Model: 'model
App.Update: 'msg -> 'model -> 'model
App.View: 'model -> 'msg UI
type INativeUI =
  interface
    abstract member Send : UIUpdate list -> unit
  end


 Native UI interface.
Multiple items
type CompilationRepresentationAttribute =
  inherit Attribute
  new : flags:CompilationRepresentationFlags -> CompilationRepresentationAttribute
  member Flags : CompilationRepresentationFlags

--------------------
new : flags:CompilationRepresentationFlags -> CompilationRepresentationAttribute
type CompilationRepresentationFlags =
  | None = 0
  | Static = 1
  | Instance = 2
  | ModuleSuffix = 4
  | UseNullAsTrueValue = 8
  | Event = 16
CompilationRepresentationFlags.ModuleSuffix: CompilationRepresentationFlags = 4
val memoize<'model,'msg (requires reference type and reference type)> : (('model -> 'msg UI) -> 'model -> 'msg UI) (requires reference type and reference type)


 Memoize view generation from model object references.
val not : value:bool -> bool
val d : System.Runtime.CompilerServices.ConditionalWeakTable<'model,'msg UI> (requires reference type and reference type)
namespace System
namespace System.Runtime
namespace System.Runtime.CompilerServices
Multiple items
type ConditionalWeakTable<'TKey,'TValue (requires reference type and reference type)> =
  new : unit -> ConditionalWeakTable<'TKey, 'TValue>
  member Add : key:'TKey * value:'TValue -> unit
  member GetOrCreateValue : key:'TKey -> 'TValue
  member GetValue : key:'TKey * createValueCallback:CreateValueCallback<'TKey, 'TValue> -> 'TValue
  member Remove : key:'TKey -> bool
  member TryGetValue : key:'TKey * value:'TValue -> bool
  nested type CreateValueCallback

--------------------
System.Runtime.CompilerServices.ConditionalWeakTable() : System.Runtime.CompilerServices.ConditionalWeakTable<'TKey,'TValue>
val view : ('model -> 'msg UI) (requires reference type and reference type)
val model : 'model (requires reference type)
System.Runtime.CompilerServices.ConditionalWeakTable.TryGetValue(key: 'model, value: byref<'msg UI>) : bool
val ui : 'msg UI (requires reference type)
System.Runtime.CompilerServices.ConditionalWeakTable.Add(key: 'model, value: 'msg UI) : unit
val text : text:string -> 'a UI


 Returns a Text display UI component.
val text : string
val ignore : value:'T -> unit
val input : text:string -> string UI


 Returns a text Input UI component.
val ev : (string -> unit) ref ref
val ui : string UI
val raise : (string -> unit)
val a : string
UI.Event: string -> unit
val button : text:string -> msg:'a -> 'a UI


 Returns a Button UI component.
val msg : 'a
val ev : (unit -> unit) ref ref
val ui : 'a UI
UI.Event: 'a -> unit
val div : layout:Layout -> list:'a UI list -> 'a UI


 Returns a section of UI components given a layout.
 The name div comes from HTML and represents a division (or section) of UI components.
val layout : Layout
Multiple items
val list : 'a UI list

--------------------
type 'T list = List<'T>
Multiple items
module List

from Main

--------------------
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
    interface IReadOnlyList<'T>
    interface IReadOnlyCollection<'T>
    interface IEnumerable
    interface IEnumerable<'T>
    member GetSlice : startIndex:int option * endIndex:int option -> 'T list
    member Head : 'T
    member IsEmpty : bool
    member Item : index:int -> 'T with get
    member Length : int
    member Tail : 'T list
    ...
val map : mapping:('T -> 'U) -> list:'T list -> 'U list
UI.UI: UI
val raise : ('a -> unit)
val a : 'a
val iter : action:('T -> unit) -> list:'T list -> unit
val i : 'a UI
val map : f:('a -> 'b) -> ui:'a UI -> 'b UI


 Returns a new UI component mapping the message event using the given function.
val f : ('a -> 'b)
val ui2 : 'b UI
val e : 'a
UI.Event: 'b -> unit
val diff : ui1:'a UI -> ui2:'b UI -> UIUpdate list


 Returns a list of UI updates from two UI components.
val ui1 : 'a UI
val update : ('c ref ref -> 'c ref ref -> unit -> unit)
val e1 : 'c ref ref
val e2 : 'c ref ref
val ev : 'c ref
val diff : (UI -> UI -> int list -> int -> UIUpdate list -> UIUpdate list)
val ui1 : UI
val ui2 : UI
val path : int list
val index : int
val diffs : UIUpdate list
module LanguagePrimitives

from Microsoft.FSharp.Core
val PhysicalEquality : e1:'T -> e2:'T -> bool (requires reference type)
val t1 : string
val t2 : string
val e1 : unit Event
val e2 : unit Event
val e1 : string Event
val e2 : string Event
val l1 : Layout
val l2 : Layout
val l : UI list
val fold : folder:('State -> 'T -> 'State) -> state:'State -> list:'T list -> 'State
val ui : UI
val snd : tuple:('T1 * 'T2) -> 'T2
val l : Layout
val h1 : UI
val t1 : UI list
val h2 : UI
val t2 : UI list
val h3 : UI
val app : model:'a -> update:('b -> 'a -> 'a) -> view:('a -> 'b UI) -> App<'b,'a>


 Returns a UI application from a UI model, update and view.
val model : 'a
val update : ('b -> 'a -> 'a)
val view : ('a -> 'b UI)
val run : nativeUI:INativeUI -> app:App<'a,'b> -> unit


 Runs a UI application given a native UI.
val nativeUI : INativeUI
val app : App<'a,'b>
val handle : ('b -> 'a UI -> 'a -> unit)
val model : 'b
val newModel : 'b
App.Update: 'a -> 'b -> 'b
val newUI : 'a UI
App.View: 'b -> 'a UI
val diff : UIUpdate list
val f : (unit -> unit)
abstract member INativeUI.Send : UIUpdate list -> unit
App.Model: 'b
type Msg =
  | Increment
  | Decrement
union case Msg.Increment: Msg
union case Msg.Decrement: Msg
type Model = int
val init : i:Model -> Model
val i : Model
val update : msg:Msg -> model:int -> Model
val msg : Msg
val model : int
val view : model:Model -> Msg UI
val model : Model
Multiple items
module UI

from Main

--------------------
type UI =
  | Text of string
  | Input of string * string Event
  | Button of string * unit Event
  | Div of Layout * UI list


 Primative UI components.


--------------------
type 'msg UI =
  {UI: UI;
   mutable Event: 'msg -> unit;}


 UI component including a message event.
val app : i:Model -> App<Msg,Model>
type Msg =
  | Reset
  | Top of Msg
  | Bottom of Msg
union case Msg.Reset: Msg
union case Msg.Top: Counter.Msg -> Msg
module Counter

from Main
union case Msg.Bottom: Counter.Msg -> Msg
type Model =
  {Top: Model;
   Bottom: Model;}
Model.Top: Counter.Model
Model.Bottom: Counter.Model
val init : top:Counter.Model -> bottom:Counter.Model -> Model
val top : Counter.Model
val bottom : Counter.Model
val init : i:Counter.Model -> Counter.Model
val update : msg:Msg -> model:Model -> Model
val msg : Counter.Msg
val update : msg:Counter.Msg -> model:int -> Counter.Model
val view : model:Counter.Model -> Counter.Msg UI
val app : top:Counter.Model -> bottom:Counter.Model -> App<Msg,Model>
type Msg =
  | Insert
  | Remove
  | Modify of int * Msg
union case Msg.Insert: Msg
union case Msg.Remove: Msg
union case Msg.Modify: int * Counter.Msg -> Msg
type Model =
  {Counters: Model list;}
Model.Counters: Counter.Model list
val init : Model
val xs : Counter.Model list
val mapi : mapping:(int -> 'T -> 'U) -> list:'T list -> 'U list
val c : Counter.Model
val v : Counter.Msg
val app : App<Msg,Model>
namespace System.Windows
namespace System.Windows.Controls
module WPF

from Main
val CreateNaiveUI : root:ContentControl -> INativeUI
val root : ContentControl
Multiple items
type ContentControl =
  inherit Control
  new : unit -> ContentControl
  member Content : obj with get, set
  member ContentStringFormat : string with get, set
  member ContentTemplate : DataTemplate with get, set
  member ContentTemplateSelector : DataTemplateSelector with get, set
  member HasContent : bool
  member ShouldSerializeContent : unit -> bool
  static val ContentProperty : DependencyProperty
  static val HasContentProperty : DependencyProperty
  static val ContentTemplateProperty : DependencyProperty
  ...

--------------------
ContentControl() : ContentControl
val createUI : (UI -> UIElement)
Multiple items
type UIElement =
  inherit Visual
  new : unit -> UIElement
  member AddHandler : routedEvent:RoutedEvent * handler:Delegate -> unit + 1 overload
  member AddToEventRoute : route:EventRoute * e:RoutedEventArgs -> unit
  member AllowDrop : bool with get, set
  member ApplyAnimationClock : dp:DependencyProperty * clock:AnimationClock -> unit + 1 overload
  member AreAnyTouchesCaptured : bool
  member AreAnyTouchesCapturedWithin : bool
  member AreAnyTouchesDirectlyOver : bool
  member AreAnyTouchesOver : bool
  member Arrange : finalRect:Rect -> unit
  ...

--------------------
UIElement() : UIElement
val c : Label
Multiple items
type Label =
  inherit ContentControl
  new : unit -> Label
  member Target : UIElement with get, set
  static val TargetProperty : DependencyProperty

--------------------
Label() : Label
Multiple items
union case UI.Input: string * string Event -> UI

--------------------
namespace System.Windows.Input
val event : string Event
val c : TextBox
Multiple items
type TextBox =
  inherit TextBoxBase
  new : unit -> TextBox
  member CaretIndex : int with get, set
  member CharacterCasing : CharacterCasing with get, set
  member Clear : unit -> unit
  member GetCharacterIndexFromLineIndex : lineIndex:int -> int
  member GetCharacterIndexFromPoint : point:Point * snapToText:bool -> int
  member GetFirstVisibleLineIndex : unit -> int
  member GetLastVisibleLineIndex : unit -> int
  member GetLineIndexFromCharacterIndex : charIndex:int -> int
  member GetLineLength : lineIndex:int -> int
  ...

--------------------
TextBox() : TextBox
val event : (string -> unit) ref
event Primitives.TextBoxBase.TextChanged: IEvent<TextChangedEventHandler,TextChangedEventArgs>
member System.IObservable.Add : callback:('T -> unit) -> unit
val t : string
property TextBox.Text: string
val async : AsyncBuilder
Multiple items
type Async =
  static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
  static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
  static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
  static member AwaitTask : task:Task -> Async<unit>
  static member AwaitTask : task:Task<'T> -> Async<'T>
  static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
  static member CancelDefaultToken : unit -> unit
  static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
  static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
  static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
  ...

--------------------
type Async<'T> =
static member Async.Start : computation:Async<unit> * ?cancellationToken:System.Threading.CancellationToken -> unit
Multiple items
type Button =
  inherit ButtonBase
  new : unit -> Button
  member IsCancel : bool with get, set
  member IsDefault : bool with get, set
  member IsDefaulted : bool
  static val IsDefaultProperty : DependencyProperty
  static val IsCancelProperty : DependencyProperty
  static val IsDefaultedProperty : DependencyProperty

--------------------
Button() : Button
val event : unit Event
val c : Button
val event : (unit -> unit) ref
event Primitives.ButtonBase.Click: IEvent<RoutedEventHandler,RoutedEventArgs>
Multiple items
val list : UI list

--------------------
type 'T list = List<'T>
val children : UIElement list
val c : StackPanel
Multiple items
type StackPanel =
  inherit Panel
  new : unit -> StackPanel
  member CanHorizontallyScroll : bool with get, set
  member CanVerticallyScroll : bool with get, set
  member ExtentHeight : float
  member ExtentWidth : float
  member HorizontalOffset : float
  member LineDown : unit -> unit
  member LineLeft : unit -> unit
  member LineRight : unit -> unit
  member LineUp : unit -> unit
  ...

--------------------
StackPanel() : StackPanel
type Orientation =
  | Horizontal = 0
  | Vertical = 1
field Orientation.Vertical: Orientation = 1
field Orientation.Horizontal: Orientation = 0
property Panel.Children: UIElementCollection
UIElementCollection.Add(element: UIElement) : int
val locatePanel : (int list -> Panel)
val loc : int list
type Panel =
  inherit FrameworkElement
  member Background : Brush with get, set
  member Children : UIElementCollection
  member HasLogicalOrientationPublic : bool
  member IsItemsHost : bool with get, set
  member LogicalOrientationPublic : Orientation
  member ShouldSerializeChildren : unit -> bool
  static val BackgroundProperty : DependencyProperty
  static val IsItemsHostProperty : DependencyProperty
  static val ZIndexProperty : DependencyProperty
  static member GetZIndex : element:UIElement -> int
  ...
property ContentControl.Content: obj
val xs : int list
val uiUpdate : (UIUpdate -> unit)
val u : UIUpdate
val element : UIElement
val c : UIElementCollection
UIElementCollection.RemoveAt(index: int) : unit
UIElementCollection.Insert(index: int, element: UIElement) : unit
Multiple items
val list : UIUpdate list

--------------------
type 'T list = List<'T>
property Threading.DispatcherObject.Dispatcher: Threading.Dispatcher
Threading.Dispatcher.Invoke<'TResult>(callback: System.Func<'TResult>) : 'TResult
   (+0 other overloads)
Threading.Dispatcher.Invoke(callback: System.Action) : unit
   (+0 other overloads)
Threading.Dispatcher.Invoke(method: System.Delegate, [<System.ParamArray>] args: obj []) : obj
   (+0 other overloads)
Threading.Dispatcher.Invoke(priority: Threading.DispatcherPriority, method: System.Delegate) : obj
   (+0 other overloads)
Threading.Dispatcher.Invoke<'TResult>(callback: System.Func<'TResult>, priority: Threading.DispatcherPriority) : 'TResult
   (+0 other overloads)
Threading.Dispatcher.Invoke(callback: System.Action, priority: Threading.DispatcherPriority) : unit
   (+0 other overloads)
Threading.Dispatcher.Invoke(method: System.Delegate, timeout: System.TimeSpan, [<System.ParamArray>] args: obj []) : obj
   (+0 other overloads)
Threading.Dispatcher.Invoke(method: System.Delegate, priority: Threading.DispatcherPriority, [<System.ParamArray>] args: obj []) : obj
   (+0 other overloads)
Threading.Dispatcher.Invoke(priority: Threading.DispatcherPriority, timeout: System.TimeSpan, method: System.Delegate) : obj
   (+0 other overloads)
Threading.Dispatcher.Invoke(priority: Threading.DispatcherPriority, method: System.Delegate, arg: obj) : obj
   (+0 other overloads)