Copy-and-update syntax for interfaces (decorator pattern) #524

Open
Overlord-Zurg opened this Issue Dec 8, 2016 · 7 comments

Comments

Projects
None yet
5 participants
@Overlord-Zurg

Overlord-Zurg commented Dec 8, 2016

I propose we add syntax to interfaces similar to the copy and update record expression syntax.

Say I want to implement an IList<_> that constrains an existing IList implementation to a maximum item count. Currently I would can do:

let withBound max (list: System.Collections.Generic.IList<'a>) =
    { new System.Collections.Generic.IList<'a> with
        member this.Add(item) =
            if list.Count >= max then failwith "Too many items" else list.Add(item)
        member this.Clear() = list.Clear()
        member this.Contains(item) = list.Contains(item)
        (* ...literally 12 more members *) }

With the proposed syntax, I could do:

let withBound max (list: System.Collections.Generic.IList<'a>) =
    { list with
        member this.Add(item) =
            if list.Count >= max then failwith "Too many items" else list.Add(item) }

This would make the decorator pattern very easy in F#, e.g.:

let myBoundedList =
    List<int>()
    |> withBound 5

Pros and Cons

The advantages of making this adjustment to F# are ...

  • Looks and works almost exactly like the existing syntax for records
  • Encourages sound(er) application of OO principles
  • Something else to lord over C# until they add it in six to eight years

The disadvantages of making this adjustment to F# are ...

  • It's work

Related Suggestions

  • Allow interfaces to be implemented by expressions #132

Affadavit (must be submitted)

Please tick this by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.
@ReedCopsey

This comment has been minimized.

Show comment
Hide comment
@ReedCopsey

ReedCopsey Dec 8, 2016

I would love this, but would still prefer a mechanism to specify the return type (ie: in your example, should it be IList<'a>, or ICollection<'a>, or ...?)

The ability to return an interface which delegates implementation to another object would be extremely useful, both in object expressions, but also in normal interface implementations, however, I think it'd need different syntax so the interface type was explicit.

Perhaps something like

    { list :> IList<'a> with
        member __.Add item =
            if list.Count >= max then failwith "Too many items" else list.Add(item) }

It seems like this could be easier to extend to normal interface definitions:

type Foo<'T> () =
    let internalList = ResizeArray<'T>()

    interface internalList :> IList<'T>

And/or:

type Foo<'T> () =
    let internalList = ResizeArray<'T>()

    interface internalList :> IList<'T> with
        member __.Add item =
            () // "override" implementation here

I would love this, but would still prefer a mechanism to specify the return type (ie: in your example, should it be IList<'a>, or ICollection<'a>, or ...?)

The ability to return an interface which delegates implementation to another object would be extremely useful, both in object expressions, but also in normal interface implementations, however, I think it'd need different syntax so the interface type was explicit.

Perhaps something like

    { list :> IList<'a> with
        member __.Add item =
            if list.Count >= max then failwith "Too many items" else list.Add(item) }

It seems like this could be easier to extend to normal interface definitions:

type Foo<'T> () =
    let internalList = ResizeArray<'T>()

    interface internalList :> IList<'T>

And/or:

type Foo<'T> () =
    let internalList = ResizeArray<'T>()

    interface internalList :> IList<'T> with
        member __.Add item =
            () // "override" implementation here
@Overlord-Zurg

This comment has been minimized.

Show comment
Hide comment
@Overlord-Zurg

Overlord-Zurg Dec 8, 2016

The first expression in the copy-and-update expression would determine the return type, same as record expressions. So, if you wanted to wrap an IList as an IList:

let withBounds x (list: IList<'a>) =
    { list with
        (...members...) }

If you wanted the returned type to be a different interface:

let withBounds x (list: IList<'a>) =
    { list :> ICollection<'a> with (...) }

With regard to the normal class definition, the decorator pattern doesn't make sense without passing in an existing instance of the interface, because then you run into the same single-inheritance/fragile-base-class problems that we want to avoid. I think it would look more like this:

type BoundedList<'a> (list: IList<'a>) =
    list with
        member __.Add(item) = //etc

...and then BoundedList<'a> would automatically be marked as implementing the interface of type IList<'a>.

Overlord-Zurg commented Dec 8, 2016

The first expression in the copy-and-update expression would determine the return type, same as record expressions. So, if you wanted to wrap an IList as an IList:

let withBounds x (list: IList<'a>) =
    { list with
        (...members...) }

If you wanted the returned type to be a different interface:

let withBounds x (list: IList<'a>) =
    { list :> ICollection<'a> with (...) }

With regard to the normal class definition, the decorator pattern doesn't make sense without passing in an existing instance of the interface, because then you run into the same single-inheritance/fragile-base-class problems that we want to avoid. I think it would look more like this:

type BoundedList<'a> (list: IList<'a>) =
    list with
        member __.Add(item) = //etc

...and then BoundedList<'a> would automatically be marked as implementing the interface of type IList<'a>.

@matthid

This comment has been minimized.

Show comment
Hide comment
@matthid

matthid Dec 8, 2016

One addition: It would be even more useful if there is a way to make the underlying field mutable, for example if we could use a ref-cell on the first position as well.

But even without that it feels like a logical addition on the first look.

matthid commented Dec 8, 2016

One addition: It would be even more useful if there is a way to make the underlying field mutable, for example if we could use a ref-cell on the first position as well.

But even without that it feels like a logical addition on the first look.

@dsyme

This comment has been minimized.

Show comment
Hide comment
@dsyme

dsyme Mar 3, 2017

Collaborator

@matthid

One addition: It would be even more useful if there is a way to make the underlying field mutable, for example if we could use a ref-cell on the first position as well.

Could you explain this a bit more please?

Collaborator

dsyme commented Mar 3, 2017

@matthid

One addition: It would be even more useful if there is a way to make the underlying field mutable, for example if we could use a ref-cell on the first position as well.

Could you explain this a bit more please?

@matthid

This comment has been minimized.

Show comment
Hide comment
@matthid

matthid Mar 16, 2017

@dsyme Sure. (Sorry for the delay).

Something like (I have no idea how a syntax would look like):

let withBound max (list: System.Collections.Generic.IList<'a>) =
    { mutable list with // I think this should be "marked" somehow, maybe allow a ref cell here?
        member this.Add(item) =
            if list.Count >= max then failwith "Too many items" else list.Add(item) }

let o1 : System.Collections.Generic.IList<'a>= ...
let o2 : System.Collections.Generic.IList<'a>= ...
let wrapper = withBound 3 o1
wrapper.list <- o2 // This

Basically a way to "replace" the wrapped object. Otherwise I need to wrap again and then need to implement all members again :). Usually I don't want to replace my code to understand IList ref and I would wrap again (making the feature unusable in those situations).

With a ref cell:

let withBound max (list: System.Collections.Generic.IList<'a>) =
    let l = ref list
    { l with // maybe a bit invisible?
        member this.Add(item) =
            if list.Count >= max then failwith "Too many items" else list.Add(item) }, l

let o1 : System.Collections.Generic.IList<'a>= ...
let o2 : System.Collections.Generic.IList<'a>= ...
let wrapper, cell = withBound 3 o1
cell := o2 // This

Does that make it clear?

matthid commented Mar 16, 2017

@dsyme Sure. (Sorry for the delay).

Something like (I have no idea how a syntax would look like):

let withBound max (list: System.Collections.Generic.IList<'a>) =
    { mutable list with // I think this should be "marked" somehow, maybe allow a ref cell here?
        member this.Add(item) =
            if list.Count >= max then failwith "Too many items" else list.Add(item) }

let o1 : System.Collections.Generic.IList<'a>= ...
let o2 : System.Collections.Generic.IList<'a>= ...
let wrapper = withBound 3 o1
wrapper.list <- o2 // This

Basically a way to "replace" the wrapped object. Otherwise I need to wrap again and then need to implement all members again :). Usually I don't want to replace my code to understand IList ref and I would wrap again (making the feature unusable in those situations).

With a ref cell:

let withBound max (list: System.Collections.Generic.IList<'a>) =
    let l = ref list
    { l with // maybe a bit invisible?
        member this.Add(item) =
            if list.Count >= max then failwith "Too many items" else list.Add(item) }, l

let o1 : System.Collections.Generic.IList<'a>= ...
let o2 : System.Collections.Generic.IList<'a>= ...
let wrapper, cell = withBound 3 o1
cell := o2 // This

Does that make it clear?

@matthid

This comment has been minimized.

Show comment
Hide comment
@matthid

matthid Mar 16, 2017

Maybe

type Foo<'T> () =
    let mutable internalList = ResizeArray<'T>() // Note the mutable here
    interface internalList :> IList<'T>

Would be enough for such situations, more explicit and less special syntax...
I'd surely love the feature, even without support for mutable :)

matthid commented Mar 16, 2017

Maybe

type Foo<'T> () =
    let mutable internalList = ResizeArray<'T>() // Note the mutable here
    interface internalList :> IList<'T>

Would be enough for such situations, more explicit and less special syntax...
I'd surely love the feature, even without support for mutable :)

@smoothdeveloper

This comment has been minimized.

Show comment
Hide comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment