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

Feature Request: go-like struct composition with splatting syntax? #42777

Closed
Borketh opened this issue Oct 23, 2021 · 25 comments
Closed

Feature Request: go-like struct composition with splatting syntax? #42777

Borketh opened this issue Oct 23, 2021 · 25 comments
Labels
feature Indicates new feature / enhancement requests

Comments

@Borketh
Copy link

Borketh commented Oct 23, 2021

The Go language has this cool feature for struct composition where if you put one struct after another's values, its values are inherited into the other.

type foo1 struct {
    bar   string
}
type foo2 struct {
    baz   string
    foo1  // foo2 now also has the field "bar" inherited from foo1
}

This seems like it would fit well into Julia.

My suggested implementation would be:

struct foo1
    bar::String
end

struct foo2
    baz::String
    foo1...
end

Thanks for taking the time to read this! It would be yet another pretty nifty trick that Julia could use.

@vtjnash vtjnash added invalid Indicates that an issue or pull request is no longer relevant feature Indicates new feature / enhancement requests labels Oct 23, 2021
@jpsamaroo
Copy link
Member

I would imagine this could be pretty easily implemented with a macro.

Also, if you want to put the fields of foo1 into foo2, why not just include an instance of foo1 in foo2 directly? The struct layout and performance should (probably) be identical, assuming that both structs are immutable.

@Borketh
Copy link
Author

Borketh commented Oct 25, 2021

@jpsamaroo because the point is to get foo2.bar, not foo2.[foo1].bar

@vtjnash invalid?

@vtjnash
Copy link
Member

vtjnash commented Oct 25, 2021

Duplicate of #4935

@vtjnash vtjnash marked this as a duplicate of #4935 Oct 25, 2021
@vtjnash vtjnash closed this as completed Oct 25, 2021
@JeffBezanson JeffBezanson removed the invalid Indicates that an issue or pull request is no longer relevant label Oct 25, 2021
@JeffBezanson
Copy link
Member

I wouldn't say this is an exact duplicate. The proposal there is to allow adding fields via explicit subtyping, while the proposal here is syntactic sugar for adding fields in a specific place, with no effect on subtyping.

@vtjnash
Copy link
Member

vtjnash commented Oct 25, 2021

That seems almost worse then somehow, since you can't write code against these fields, as they don't participate in dispatch anywhere, and there's no name for them.

@quinnj
Copy link
Member

quinnj commented Oct 25, 2021

I think you're way over-complicating the issue here @vtjnash; I think this is purely a syntax sugar request; at type-definition time, look up the "splatted type"'s fields, and insert them into the current type definition as if the user had typed them.

@Borketh
Copy link
Author

Borketh commented Oct 25, 2021

Can we actually discuss this without shutting me down please?
Marking this as invalid, duplicate of something vaguely related, and closing it without giving it a mote of consideration doesn't speak well of a best practice for anything.

@Borketh
Copy link
Author

Borketh commented Oct 25, 2021

Here's a more concrete example

struct Child
    name::String
    age::Int
    hobbies:Vector{String}
end

struct Student
    Child...
    currentclass::String
    grade::Union{Int, Char}
end

Bobbert = Child("Bobbert Smythe", 9, ["Gaming", "Sewing", "Horseback Riding"])
BobbertStudent = Student("Bobbert Smythe", 9, ["Gaming", "Sewing", "Horseback Riding"], "Math", 76)

# this would make sense with regular splat syntax too

Rowena = Child("Rowena Ravenclaw", 16, ["Magic", "Ravens"])
RowenaStudent = Student(Rowena..., "Charms", 'A')

@KristofferC KristofferC reopened this Oct 25, 2021
@JeffBezanson
Copy link
Member

@AstroFloof Yes I think this absolutely can be discussed. Thank you for the clarifying example; it always helps to have something more than foo and bar :)

@tkf
Copy link
Member

tkf commented Oct 25, 2021

FYI, the official term in Go is "type embedding." Ref: a short section in Effective Go

There seems to be a discussion for removing this for Go 2: golang/go#22013. It does not necessarily mean it's a bad feature for Go and/or Julia, but it's probably worth summarizing their analysis on pros and cons.

@Borketh
Copy link
Author

Borketh commented Oct 25, 2021

I don't know much Go, but one of my friends and I have this inside joke/dispute of Julia vs. Go, and he showed me this feature of that language. Impressed, I thought this might easily translate to Julia using the above syntax, or something similar.

As I understand it, it inherits class/struct attributes like this in python.

class Plant
    FoodPreferences = []
    Food = ""
    Size = 0

    def grow(self):
        if self.Food:
            self.Size += 1

class Fuchsia(Plant)
    FoodPreferences = ["Fuchsia Food"]  # Dammit Jim! I'm a programmer not a botanist.
    # Food, Size, and grow() are part of the Fuchsia class
# etc

@vtjnash
Copy link
Member

vtjnash commented Oct 25, 2021

If the goal is discussion, I recommend converting this to a Discussion or posting on Discourse, since that is not the general purpose of an issue tracker.

In your example, Julia does not inherit methods (which was the point of the issue I linked as duplicate), only the fields.

@JeffBezanson
Copy link
Member

My understanding is that the whole point is to "inherit" just the fields, i.e. Student would not be a subtype of Child, just share its fields. So a method f(::Child) would not apply to a Student --- @AstroFloof is that the intent?

@Borketh
Copy link
Author

Borketh commented Oct 26, 2021

Yeah that's what I meant. I just provided the closest example in py that wasn't quite the same.

I opened this here, because as far as I was aware, this was the place to put feature requests for the language.

@GregPlowman
Copy link
Contributor

GregPlowman commented Oct 27, 2021

I would find this syntax helpful.

I would imagine this could be pretty easily implemented with a macro.

This is what I do now. Except my macro defines the fields to be splatted, rather than extracting them from a concrete struct.

My understanding is that the whole point is to "inherit" just the fields, i.e. Student would not be a subtype of Child, just share its fields. So a method f(::Child) would not apply to a Student

Yes, I think that is the intention in this example.
However, couldn't inherited fields work with subtyping from an abstract type?

abstract type AbstractPerson end

struct Person <: AbstractPerson
    name::String
    age::Int
end

struct Student <: AbstractPerson
    name::String
    age::Int
    #Person...
    currentclass::String
    grade::Union{Int, Char}
end

name(p::AbstractPerson) = p.name
age(p::AbstractPerson) = p.age

lisa = Person("Lisa", 23)
fred = Student("Fred", 8, "Math", 76)
age(lisa)
age(fred)

@martinholters
Copy link
Member

FWIW, this can in principle be achieved with a macro:

julia> macro splatembed(expr::Expr)
           fields = Any[]
           for f in expr.args[3].args
               if isa(f, Expr) && f.head === :(...)
                   T = Base.eval(__module__, f.args[1])
                   for n in fieldnames(T)
                       push!(fields, :($(n)::$(fieldtype(T, n))))
                   end
               else
                   push!(fields, f)
               end
           end
           return Expr(:struct, expr.args[1], expr.args[2], Expr(:block, fields...))
       end
@splatembed (macro with 1 method)

julia> struct Child
           name::String
           age::Int
           hobbies::Vector{String}
       end

julia> @splatembed struct Student
           Child...
           currentclass::String
           grade::Union{Int, Char}
       end

julia> dump(Student)
Student <: Any
  name::String
  age::Int64
  hobbies::Vector{String}
  currentclass::String
  grade::Union{Char, Int64}

@fredrikekre
Copy link
Member

What happens if Child has type parameters?

@martinholters
Copy link
Member

Are you asking what does my quick-and-dirty proof-of-concept macro do then or what would be the desired outcome?
The macro does:

julia> struct Foo{T}
           x::T
           y::Vector{T}
       end

julia> @splatembed struct Bar1
           Foo...
       end

julia> dump(Bar1)
Bar1 <: Any
  x::Any
  y::Vector{T} where T

julia> @splatembed struct Bar2
           Foo{Int}...
       end

julia> dump(Bar2)
Bar2 <: Any
  x::Int64
  y::Vector{Int64}

julia> @splatembed struct Bar3{T}
           Foo{T}...
       end
ERROR: LoadError: UndefVarError: T not defined

I'd say the result for Bar1 and Bar2 is ok. For Bar3, one might hope to get this expanded to

struct Bar3{T}
    x::T
    y::Vector{T}
end

and that should also be possible, but needs a more sophisticated macro, obviously.

@fredrikekre
Copy link
Member

Yea sorry, I meant what the desired outcome would be in a case like

struct Foo{T}
    x::T
end

struct Bar
    Foo...
end

should the parameters of Foo be appended to Bar or should you have to specify them like in your example (seems fine for a parameter like Int, but typical Julia code can have quite many and long parameter lists)?

@JeffBezanson
Copy link
Member

I think we'd have to require the splatted type to be fully instantiated. So you could do

struct Bar{T}
    Foo{T}...
end

or

struct Bar
    Foo{Int}...
end

but not Foo....

@Borketh
Copy link
Author

Borketh commented Oct 27, 2021

Unless the parent struct has type-dependent attributes I don't think it is necessary...

struct Foo{T} where T <: Integer  # i don't know if that's valid syntax
    stuff::Vector{T}
end

struct Bar{T2}
    other::T2
    Foo{Int16}...
end

@jonas-schulze
Copy link
Contributor

Another difference to #4935 is that this could be used like a mixin by splatting more than one other type. This would at least require that the types of a fields splatted into from different structs are uniquely defined by their name, i.e. the splatted structs may both contain a commonfield::CommonType but not commonfield::T1 and commonfield::T2.

@mhinsch
Copy link

mhinsch commented Jun 10, 2023

For future reference, there's a package that does exactly what is being discussed in this thread: CompositeStructs.jl.

@Borketh
Copy link
Author

Borketh commented Jun 13, 2023

Oh sweet! I'd forgotten about this but that package is great! I'll close this now in favour of just using that then.

@Borketh Borketh closed this as not planned Won't fix, can't repro, duplicate, stale Jun 13, 2023
@mhinsch
Copy link

mhinsch commented Jun 13, 2023

Oh sweet! I'd forgotten about this but that package is great! I'll close this now in favour of just using that then.

One thing to keep in mind, though, while the package works well for straightforward use, there are corner cases where it breaks (all of those I have encountered so far seem fixable, though). Plus, using it in combination with @kwdef leads to duplicate creation of objects. As far as I can tell fixing this one would require either lifting the behaviour to language level or providing full reflection access to the default values of keyword parameters.
Altogether I think it's a good solution, but having it as a language feature would be much cleaner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Indicates new feature / enhancement requests
Projects
None yet
Development

No branches or pull requests