Skip to content

Feature Request: @generated structs #49187

@MasonProtter

Description

@MasonProtter

Warning: this is pretty speculative and surely quite hard to implement and we should have an issue for it.


I think it would be very useful if we had a (sparingly-used) way of generating a struct layout based on its type parameters. This would be analogous to the way we can currently generate a function body based on its type signature. I looked around and couldn't find a pre-existing issue, but this also isn't a very searchable topic.

Here are a few example things this could be used for:

Simpler SArray, and MArray which supports non-isbits types

Click me

Implementing these would be pretty much trivial if you had a @generated struct. Something like

@generated struct SArray{Size, T, N, IsMutable} <: AbstractArray{T, N}
    fields = map(1:prod(Size)) do i
        name = Symbol(:_, i)
        :($name :: $T)
    end
    Expr(:block, Expr(:mutable, IsMutable),  fields...)
end

This would make it so that if someone wrote say

SArray{(2, 2), String, 2, true}

that would represent something like

mutable struct SArray_2_2_String_2_true
    _1 :: String
    _2 :: String
    _3 :: String
    _4 :: String
end

And unlike the current implementation of MArray, this would support setfield! on the actual fields we care about letting us easily implement setindex!.

Even more powerfully, SArray could use this to decide that if say you gave it SArray{1000000000, Float64, 1, false}, that is way way too big to profitably store as an inline struct and instead heap allocate a vector or something to use as its storage.

Having a general way to melt, mutate, and then freeze an immutable struct

Click me
@generated struct Melted{T}
    fields = map(1:fieldcount(T)) do i
        name = fieldname(T, i)
        type = fieldtype(T, i)
        :($fieldname :: $fieldtype)
    end
    constructor = quote
        Melted(x::T) where {T} = new{T}($((:(getfield(x, $i) for i in 1:fieldcount(T))...))
    end
    Expr(:block, Expr(:ismutable, true), fields...,  constructor)
end

so that one could e.g. write

Melted{Complex{Float64}}

and get a struct equivalent to

mutable struct Melted_Complex_Float64
    re::Float64
    im::Float64
    MeltedComplexFloat64(x::ComplexFloat64) = new(getfield(x,1), getfield(x, 2))
end

People can then freely do things like e.g.

let m = Melted(big(1) // big(2))
    m.num = big(2)
    m.den = big(3)
    Rational(m)
end 

Note that this way we do not bypass the inner constructor of Rational.

Packages like Accessors.jl would not become unnecessary, but instead would have their scope reduced to intelligently
dealing with the properties and constructors of a type, and this would become an additional tool in their toolkit.

Forbidding specific values from a type signature

Click me

This is maybe too frivilous a use for what would likely be quite heavy machinery, but currently we have no way to put restrictions on the values in a type signature, and we have to rely on inner constructors to reject them. That is, I can write things like

Array{Float64, -1000.1}

and this is a perfectly valid type, it just will be rejected by all of its inner constructors. Having @generated structs though could allow one to forbid people from even representing an invalid value in a type signature just like how we currently can reject invalid types in a signature:

julia> struct Blah{T <: Integer} end
           

julia> Blah{String}
ERROR: TypeError: in Blah, in T, expected T<:Integer, got Type{String}

Compactified structs

Click me

Take for example Unityper.jl which takes in an expression like

@compactify begin
    @abstract struct Foo
        common_field::Int = 1
    end
    struct a <: Foo
        a::Bool = true
        b::String = "hi"
    end
    struct b <: Foo
        a::Int = 1
        b::Complex = 1 + im
    end
end;

and then compactifies these structs into one concrete struct with a minimal memory layout:

julia> dump(a())
Foo
  common_field: Int64 1
  ###a###2: Int64 1
  ###Any###3: String "hi"
  ###tag###4: var"###Foo###1" ₋₃₋₁₂₉Foo₋__a₋₃₋₁₉₉₂₋₋

julia> dump(b())
Foo
  common_field: Int64 1
  ###a###2: Int64 1
  ###Any###3: Complex{Int64}
    re: Int64 1
    im: Int64 1
  ###tag###4: var"###Foo###1" ₋₃₋₁₂₉Foo₋__b₋₃₋₁₉₉₂₋₋

julia> fieldtypes(Foo)
(Int64, Int64, Any, var"###Foo###1")

This works somewhat well, but cannot work currently if we wanted Foo to be a parametric type with parametric fields. With a @generated struct, we could generate a compactified layout precisely tailored to a set of parameters.


Just like @generated functions, this would be pretty heavy duty stuff that regular users shouldn't be doing, but I think it'd
allow authors of serious packages to do a lot of things that currently aren't possible (and in some cases, stop them from doing some worrying pointer shenanigans that doesn't generalize well), so I think having this feature should be an eventual goal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions