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

Rob Lyndon

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.

TheQuickBrownFox

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) = ...

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

From Dev

How can I put this DRY code into a for loop?

From Dev

How to DRY out this ruby code

From Dev

How can i keep my code dry in ajax requests?

From Dev

How can i make these 3 lines of code more DRY

From Dev

How can I make my JavaScript code more DRY?

From Dev

How can I check arguments in a method from an interface without breaking the DRY principle

From Dev

How to design a Fluent Interface?

From Dev

How can I set the limit on a Fluent Query?

From Dev

How can I DRY this series of conditional statements?

From Dev

Use Fluent Interface with less Code

From Dev

Use Fluent Interface with less Code

From Dev

How to handle a fluent interface where each step can be a terminal operation?

From Dev

How can I override this method in this interface (see code)?

From Dev

How can I copy the IDisposable interface "automatic" code

From Dev

how do I DRY out this ruby if-then statement?

From Dev

How can I find out what this ffmpeg error code means?

From Dev

How can I find out the type of sparql query using the code?

From Dev

Understanding of How to Create a Fluent Interface

From Dev

How to create a Fluent Interface with Generics

From Dev

how to DRY this code?

From Dev

How to DRY up this code

From Dev

How can I handle using multiple variables depending on which element of an iterator I am using? Aka, how can I DRY up this ruby code?

From Dev

How can I use code snippets in F#

From Dev

How can I use code snippets in F#

From Dev

How can I generate both standard accessors and fluent accessors with lombok?

From Dev

How can I automatically register all my fluent validators with Unity?

From Dev

How can I compare null and string.Empty (or "") in fluent assertions?

From Dev

How can I use Fluent NHibernate to discriminate on a column of a parent relationship

From Dev

How can I use Fluent without modifying the first variable

Related Related

  1. 1

    How can I put this DRY code into a for loop?

  2. 2

    How to DRY out this ruby code

  3. 3

    How can i keep my code dry in ajax requests?

  4. 4

    How can i make these 3 lines of code more DRY

  5. 5

    How can I make my JavaScript code more DRY?

  6. 6

    How can I check arguments in a method from an interface without breaking the DRY principle

  7. 7

    How to design a Fluent Interface?

  8. 8

    How can I set the limit on a Fluent Query?

  9. 9

    How can I DRY this series of conditional statements?

  10. 10

    Use Fluent Interface with less Code

  11. 11

    Use Fluent Interface with less Code

  12. 12

    How to handle a fluent interface where each step can be a terminal operation?

  13. 13

    How can I override this method in this interface (see code)?

  14. 14

    How can I copy the IDisposable interface "automatic" code

  15. 15

    how do I DRY out this ruby if-then statement?

  16. 16

    How can I find out what this ffmpeg error code means?

  17. 17

    How can I find out the type of sparql query using the code?

  18. 18

    Understanding of How to Create a Fluent Interface

  19. 19

    How to create a Fluent Interface with Generics

  20. 20

    how to DRY this code?

  21. 21

    How to DRY up this code

  22. 22

    How can I handle using multiple variables depending on which element of an iterator I am using? Aka, how can I DRY up this ruby code?

  23. 23

    How can I use code snippets in F#

  24. 24

    How can I use code snippets in F#

  25. 25

    How can I generate both standard accessors and fluent accessors with lombok?

  26. 26

    How can I automatically register all my fluent validators with Unity?

  27. 27

    How can I compare null and string.Empty (or "") in fluent assertions?

  28. 28

    How can I use Fluent NHibernate to discriminate on a column of a parent relationship

  29. 29

    How can I use Fluent without modifying the first variable

HotTag

Archive