Feature request: implicit use of struct member


#1

In the spirit of keeping code clean and terse I think it would be great to be able to implicitly use (reference) a ‘special’ member of a struct. My apologies if this is already an existing feature that I have overlooked (don’t think so though).

Consider an example. Let’s say I have something like this:

mutable struct Layer
    layer::Array{Int64,2}
    res::Array{Int64,2}
    back::Function
    forward::Function
end

Then, within functions using Layers you might find a few references to the various members but many references to:

# Assume 'layers' is a previously defined 'Vector' of 'Layer'
layers[ x ].layer    # long clunky reference

# let's shorten it...
l = layers     # line needlessly wasted just to shorten expression
l[ x ].layer   # Still ugly ...who knows how many times this will be repeated throughout our code

Sure, the later is better than the first but can we do better?
I don’t want to get in to recommending any specifics on the syntax but …What if I could omit .layer for all references? I think it would be very convenient to make that member anonymous. Other members would have to be specified in the normal way.

What it might look like in practice:

mutable struct Layer
    Array{Int64,2}             # To specify we want anonymous/implicit data of this type
    res::Array{Int64,2}
    back::Function
    forward::Function
end

# Create Layer()
julia> layer = Layer(d, σ, δ) # We are obviously not assigning to an anonymous member ..we are creating the struct 

julia> layer = [ 1 2 3; 4 5 6; 7 8 9]   # Assign data to the (now existing) anonymous member of layer

julia> layer[1,2]   # access data
2

One might expect the criticism that anonymous members make the code less clear but I would instead argue that it avoids redundancy. Notice that layers[x] is a layer and then we add a dot and say layer again!?! This seems to be quite common. Another way I’ve seen this issue in code in the wild is with member names such as data. Well, yes! …of course it’s data! It’s all data, so why use that word? The obvious answer is that generally most languages require every member to have a name. But why should it have to have a name if it is given one on creation? That is where redundancy is generated.

As an example I’ve seen things similar this:

mutable struct MyFileFormat
    data::Array{Int64,2}             # To specify we want anonymous/implicit data of this type
    length                        # other arbitrary pieces of data
    ...
end

MyFile = MyFileFormat( ... )

# Since the data is the whole point of this. The data is the file!
# why should I have to redundantly specify it?
MyFile.data[ ]     # Not pretty :(

MyFile[ ]     # Better :)

I’m looking forward to hearing peoples’ thoughts this idea to see if it’s feasable.

:grin: cheers!


#2

My primary concern would be about readability. One would need to look up non-local information to know that l[x] is actually l[x].layer.

In case you are treating the fields of a vector as a vector of fields a lot, consider


#3

@Tamas_Papp I believe, that the need to look up what the struct is (how it is made up) would likely be there either way. In the case that it is an anonymous member or not.

As for readability, I think that’s more of an implementation detail. Consider this:

layers[x].layer
# Vs

layer[x]

#4

I don’t like this because it makes it very easy to confuse the object itself with some member of it, and to say that’s an important distinction would be understating the matter.

One suggestion would be to define (l::Layer)() = l.layer. Probably not quite what you’re looking for, but I can’t really imagine a way to do what I think you are suggesting without introducing syntax in which the aforementioned ambiguity is a problem.

Where I might agree with you is that it might be nice to have a language construct which does something like getfield.(layers, :layer) in some nice and readable way which is useful in many different contexts. Perhaps a convenient macro? I don’t have any great ideas off the top of my head about what this might look like.


#5

Well @ExpandingMan, “I don’t like it” is a valid criticism since the language should be intuitive and easy for all to use.

As with other features this one could potentially be abused I would hope that it’s benefit would outweigh that risk. That’s certainly up for debate though. Ideally, it would only be used when it contributes to the readability of the code.

I would like to mention one additional observation. You mentioned the distinction between an object and it’s member. I think that your point is in many cases quite valid but perhaps not as much in the examples I gave.

For example, the layer is the Layer, conceptually speaking. And the data is the file in the same sense. The other members could be described as attachments, meta-data, or whatever …and also functions. In this type of situation, I think this method has merit. In fact, it could be compared to an array which has an index and size and so forth but you do not reference the array by a member. You just add brackets to the name and index the data directly.


#6
julia> mutable struct Layer
           data::Array{Int64,2}             # To specify we want anonymous/implicit data of this type
           res::Array{Int64,2}
       end

julia> import Base.setindex!
julia> import Base.getindex

julia> getindex(x::Layer, y...) = x.data[y...];
julia> getindex(x::Layer) = x.data;
julia> function setindex!(x::Layer, y, z...)  x.data[z...] = y end;
julia> function setindex!(x::Layer, y)  x.data = y end;

Part of your request is implemented now:

julia> l = Layer([1 2 3;4 5 6], [1 1;2 2])
Layer([1 2 3; 4 5 6], [1 1; 2 2])

julia> l[] = [3 4 5;]
1×3 Array{Int64,2}:
 3  4  5

julia> l
Layer([3 4 5], [1 1; 2 2])

julia> l[1,2]
4

julia> l[1,3] = 7
7

julia> l
Layer([3 4 7], [1 1; 2 2])

l = [3 4 5;] instead of l[] = [3 4 5;] is IMO against language design because l is variable and has to be rebindable!


#7

This feature looks exactly like https://github.com/JuliaLang/julia/issues/9821 (and has the same problems).


#8

I like using the @delegate macro hiding in DataSructures for this.

julia> mutable struct Layer
                  data::Array{Int64,2}
                  res::Array{Int64,2}
              end

julia> using DataStructures

julia> DataStructures.@delegate Layer.data [Base.setindex!,Base.getindex]
julia> l = Layer([1 2 3;4 5 6], [1 1;2 2])
Layer([1 2 3; 4 5 6], [1 1; 2 2])

julia> l[] = [3 4 5;] # doesn't work without additional methods

julia> l
Layer([1 2 3; 4 5 6], [1 1; 2 2])

julia> l[1,2]
2

julia> l[1,3] = 7
7

julia> l
Layer([1 2 7; 4 5 6], [1 1; 2 2])


#9

I don’t have much to add in the way of implementation, but I wanted to point out that this seems awfully close to a request for metadata on a type. That is, it would be nice if you could do something like:

layers[1] # => returns a Layer
getmeta(layers[1]) # => returns a struct containing fields `res`, `back`, `forward`

For what it’s worth, Ruby, Clojure, and Lua all have such a concept.

Actually, thinking about it a bit more, it probably wouldn’t be too difficult to define a macro that simultaneously defines a type and a dictionary keyed by that type that could contain the corresponding metadata…(implementation is left as an exercise for the reader :wink: )


#10

Also, there is @forward from Lazy.jl for the same purpose.

The fact that there are multiple implementations may be an argument for the usefulness of this feature, and potential inclusion into Base.


#11

This look similar to inheritance for me, but looks also like composition. Maybe this is a boundary between them, would it be less fragile than https://github.com/JuliaLang/julia/issues/9821 constructing a composition (that gets automatically defined methods) based on classical inheritance rules?

The usecase posted is not really motivating enough without showing an advantage usable by the current ecosystem.

3 Toughts:
That field would need at least a name/way to get access to it.
And layer = [ 1 2 3; 4 5 6; 7 8 9] is not well defined since = (the assignment) is not overridable in Julia for now and so layer would just be of type array{Int,2}.
The fact that thre are mulitple implementations of this may be also driven by people minds still trying to use inheritance.


#12

Looking at this from a slightly different angle, we could compare it to something that already exists such as an array (yeah, I kinda’ hinted at this above).

An array in the strictest sense is just an iterable collection of values but in practice there’s a need to associate values with the array to describe its dimentions, location in memory and potentially other details. But we still don’t have to reference the array values with a name, it is annonymous because conceptually the values are the array and the other data are just details of implementation. This principal is not unique to arrays.

It would be burdensome/sloppy to have to always reference an array’s values like this:

MyArray.data[ n1, n2 ] = 3     #  :( 

But the fact is(correct me if I’m wrong) that an array ussually has more data associated with it than just the set of values

In that case, what I proposed is really just a more versatile implementation of the same principal that is already in use. It would be more versatile because:

  • you would’t be restricted to one data type (itterable sets of values).
  • it has the potential for making cleaner code.
  • a bit less typing.

Think of an image; it has many bits of data associated with it but then there’s the image data …that data is the image. In the case of a .bmp, for example, it may be an array. It seems completely reasonable to me to be able to refer to that data without having to give it a second name.

Although, I’ve given mostly examples associated with arrays I do not mean to imply that the only use case is with arrays.

I do recognize there are both pros and cons to this proposal but I am very intersted in determining if it is feasable. Thanks for all the great comments :sunglasses:


#13

I really would like a feature like this.
Everytime I wrap another data type, I find myself implementing all the methods that apply to that other datatype, just so that I can delegate to the wrapped type.
For example:

+(x::MyNumber, y::MyNumber) = x.value + y.value
+(x::Number , y::MyNumber) = x + y.value
+(x::MyNumber, y::Number ) = x.value + y

It would be helpful to have a dispatch mechanism that can dispatch on a data type, while ignoring the function.
If every call where to go thru something like

apply(f, xs…)

I could implement a methods like

apply(f, x::MyNumber) = f(x.value)
apply(f, x::MyNumber, y::MyNumber) = f(x.value, y.value)

It would be very similar to the way broadcast works:

broadcast(f, A…)

which works for any function f and I can write specialized methods for it as needed.


#15

The return value of setindex! should be the Layer argument, so this delegation would be (slightly) wrong.


#16

This is a common request, but in other languages that have class inheritance, it is known as the “fragile base class problem”.

Also, shouldn’t that have a return type of MyNumber too:

+(x::MyNumber, y::MyNumber) = MyNumber(x.value + y.value)

(although, in this specific case, you would probably actually define promotion rules instead of the later two methods)
How would you propose dealing with this problem (of sometimes needing to wrap the return value in some way) in general?


#17

I’ve come to the conclusion that there isn’t an easy solution to this.

I found three major patterns:

  • functions like isodd(x::MyNumber) = isodd(x.value)
  • functions like abs(x::MyNumber) = MyNumber(abs(x))
  • functions like sqrt(x::MyNumber) = (sqrt(MyNumber))(sqrt(x))
    The last one assumes that the function results in a different type of number.

Similar patterns occur when all arguments of n-ary functions are of type MyNumber.
But it may get more complicated if the arguments are of different type.

My suggestion for apply(f, xs...) would only cover one of those three major patterns. One would still have to explicitly specify the functions for the other patterns. Since none of the patterns applies to a large majority of cases, I withdraw my suggestion.