Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I DRY out this F# code? (Fluent Interface)

So this is some of the wettest code I've ever written. But it's useful, which is annoying. The reason for all the repetition is because I want to keep the interface fluent. If I augmented the base class (which happens to be View in this case), it would only give back an instance of View, which would prevent me from doing something like

let label = theme.CreateLabel().WithMargin(new Thickness(5.0)).WithText("Hello")

because the Label.Text property is not implemented by the View base class.

So here is my fluent interface. Get ready. It's ugly, and repetitive. But it also works, and is convenient to use.

Have I missed an obvious way to DRY it out?

module ViewExtensions =
    let private withTwoWayBinding<'TElement, 'TProperty, 'TViewModel, 'TView when 'TView :> IViewFor<'TViewModel>>(viewModel: 'TViewModel, view: 'TView, viewModelProperty: Expr<'TViewModel -> 'TProperty>, viewProperty: Expr<'TView -> 'TProperty>) (element: 'TElement) = 
        view.Bind(viewModel, ExpressionConversion.toLinq viewModelProperty, ExpressionConversion.toLinq viewProperty) |> ignore
        element
    let private withHorizontalOptions<'TElement when 'TElement :> View> options (element: 'TElement) =
        element.HorizontalOptions <- options
        element
    let private withVerticalOptions<'TElement when 'TElement :> View> options (element: 'TElement) =
        element.VerticalOptions <- options
        element
    let private withAlignment<'TElement when 'TElement :> View> horizontalOptions verticalOptions (control: 'TElement) =
        control |> withHorizontalOptions horizontalOptions |> withVerticalOptions verticalOptions
    let private withMargin<'TElement when 'TElement :> View> margin (element: 'TElement) = 
        element.Margin <- margin
        element
    let private withActions<'TElement> (actions: ('TElement -> unit)[]) (element: 'TElement) = 
        for action in actions do action(element)
        element
    type Xamarin.Forms.Entry with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) = withTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) this
        member this.WithMargin(margin) = withMargin margin this
        member this.With(actions) = withActions actions this
        member this.With(action: Entry -> unit) = this.With([|action|])
    type Xamarin.Forms.Grid with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithMargin(margin) = withMargin margin this
        member this.With(actions) = withActions actions this
        member this.With(action: Grid -> unit) = this.With([|action|])
    type Xamarin.Forms.StackLayout with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithMargin(margin) = withMargin margin this
        member this.With(actions) = withActions actions this
        member this.With(action: StackLayout -> unit) = this.With([|action|])
    type Xamarin.Forms.Button with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithMargin(margin) = withMargin margin this
        member this.WithText(text) = this.Text <- text; this
        member this.With(actions) = withActions actions this
        member this.With(action: Button -> unit) = this.With([|action|])
    type Xamarin.Forms.Switch with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) = withTwoWayBinding(viewModel, view, viewModelProperty, viewProperty) this
        member this.WithMargin(margin) = withMargin margin this
        member this.With(actions) = withActions actions this
        member this.With(action: Switch -> unit) = this.With([|action|])
    type Xamarin.Forms.Label with 
        member this.WithHorizontalOptions(options) = withHorizontalOptions options this
        member this.WithVerticalOptions(options) = withHorizontalOptions options this
        member this.WithAlignment(horizontalOptions, verticalOptions) = withAlignment horizontalOptions verticalOptions this
        member this.WithMargin(margin) = withMargin margin this
        member this.WithText(text) = this.Text <- text; this
        member this.With(actions) = withActions actions this
        member this.With(action: Label -> unit) = this.With([|action|])

UPDATE

So thanks to your help, the answer is yes, I was missing something obvious. As TheQuickBrownFox explained, if I change the fluent interface to something of the form

let label = theme.CreateLabel() |> withMargin(new Thickness(5.0)) |> withContent("Hello")

then the monster you see above can be replaced in its entirety by

module ViewExtensions =
    let withTwoWayBinding<'TElement, 'TProperty, 'TViewModel, 'TView when 'TView :> IViewFor<'TViewModel>>(viewModel: 'TViewModel, view: 'TView, viewModelProperty: Expr<'TViewModel -> 'TProperty>, viewProperty: Expr<'TView -> 'TProperty>) (element: 'TElement) = 
        view.Bind(viewModel, ExpressionConversion.toLinq viewModelProperty, ExpressionConversion.toLinq viewProperty) |> ignore
        element
    let withHorizontalOptions options (element: #View) = element.HorizontalOptions <- options; element
    let withVerticalOptions options (element: #View) = element.VerticalOptions <- options; element
    let withAlignment horizontalOptions verticalOptions element = element |> withHorizontalOptions horizontalOptions |> withVerticalOptions verticalOptions
    let withMargin margin (element: #View) = element.Margin <- margin; element
    let withCaption text (element: #Button) = element.Text <- text; element
    let withText text (element: #Entry) = element.Text <- text; element
    let withContent text (element: #Label) = element.Text <- text; element
    let withSetUpActions<'TElement> (actions: ('TElement -> unit)[]) (element: 'TElement) = (for action in actions do action(element)); element
    let withSetUpAction<'TElement> (action: 'TElement -> unit) = withSetUpActions([|action|])

This code deletion is very pleasing indeed.

like image 615
Rob Lyndon Avatar asked Aug 02 '16 15:08

Rob Lyndon


People also ask

How do you dry wet banknotes?

To dry out wet currency, put your bills near a small to medium-sized box fan. Alternatively, you can use a hand blow dryer to save time. In either case, make sure to use a low air speed so you don't blow your bills away.

Which drying method is the best?

Dehydrators. Produce the best quality product as compared to other methods of drying. Most food dehydrators have an electric element for heat and a fan and vents for air circulation.


1 Answers

The idiomatic F# approach to fluent interfaces is just to use the pipe forward operator |>

module ViewHelpers
    let withMargin margin element = ...
    let withText text element = ...

open ViewHelpers

let label = theme.CreateLabel() |> withMargin (new Thickness(5.0)) |> withText "Hello"

I think you can also shorten your function signatures using flexible types:

let withMargin margin (element: #View) = ...
like image 112
TheQuickBrownFox Avatar answered Sep 27 '22 03:09

TheQuickBrownFox