Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
3 tasks done
Overlord-Zurg opened this issue Dec 8, 2016 · 11 comments
Closed
3 tasks done

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

Overlord-Zurg opened this issue Dec 8, 2016 · 11 comments

Comments

@Overlord-Zurg
Copy link

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

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
Copy link

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
Copy link
Author

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
Copy link

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
Copy link
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
Copy link

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
Copy link

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
Copy link
Contributor

Related: dotnet/csharplang#477

@matthid
Copy link

matthid commented Jun 5, 2019

Considering #132 #195 and #555 I think for object expressions the same syntax should be used.
Therefore, I'd suggest:

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

With the same semantics as in #132 (comment)

@matthid
Copy link

matthid commented Jun 5, 2019

Note that regarding this comment I feel like we should support any expression after by, including a ref cell:

let withBound max (list: System.Collections.Generic.IList<'a>) =
    let l = ref list
    { new System.Collections.Generic.IList<'a> by !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

@dsyme
Copy link
Collaborator

dsyme commented Sep 25, 2021

Closing this as a duplicate of #164 , it should all be considered as part of one thing

@dsyme dsyme closed this as completed Sep 25, 2021
@Nhowka
Copy link

Nhowka commented Feb 4, 2023

Note that regarding this comment I feel like we should support any expression after by, including a ref cell:

let withBound max (list: System.Collections.Generic.IList<'a>) =
    let l = ref list
    { new System.Collections.Generic.IList<'a> by !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

@dsyme I have a working implementation for the object expression case like this one. The exact code works, only changing the by keyword for a simple equals sign. Same as the code on #555, but I couldn't understand how to fit it with #164. Could we revisit that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants