# RFC: macro for checking that a struct is concretely typed

**URL:** <https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017>\
**Category:** Performance\
**Tags:** struct, type-stability\
**Created:** [October 30, 2024, 7:14pm UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017 "2024-10-30T19:14:56Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [October 30, 2024, 7:14pm UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/1 "2024-10-30T19:14:56Z")

</div>

This week I gave a tutorial on performance in Julia and helped a few beginners optimize their code. One of the most frequent mistakes was structs with abstractly-typed fields.  
As a pedagogical tool, @serenity4 and I wrote a small (unregistered) package to help diagnose and fix these issues. Here it is:

> **[GitHub - gdalle/CheckConcreteStructs.jl](https://github.com/gdalle/CheckConcreteStructs.jl)**
>
> Contribute to gdalle/CheckConcreteStructs.jl development by creating an account on GitHub.

A few examples:

```julia
julia> using CheckConcreteStructs

julia> @check_concrete struct A
           x
       end
ERROR: AbstractFieldError in struct `A`: field `x` with
declared type `Any` is not concretely typed.

julia> @check_concrete struct B
           x::AbstractVector{Float64}
       end
ERROR: AbstractFieldError in struct `B`: field `x` with
declared type `AbstractVector{Float64}` is not concretely typed.

julia> @check_concrete struct C
           x::Vector{<:Real}
       end
ERROR: AbstractFieldError in struct `C`: field `x` with
declared type `Vector{<:Real}` is not concretely typed.

julia> @check_concrete struct D{T,V<:AbstractVector}
           x::V
       end

```

Feel free to take it out for a spin, test it on your own fancy structs and corner cases, open issues if needed! We’re mostly curious to see:

- if there is interest for such a package
- whether our metaprogramming-based approach seems correct
- whether the package should be registered / merged with [ConcreteStructs.jl](https://github.com/jonniedie/ConcreteStructs.jl) (ping @jonniedie) / integrated to Julia itself

---

<div class="post-metadata">

**Author:** ![jonniedie](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jonniedie/32/12842_2.png) [@jonniedie](https://discourse.julialang.org/u/jonniedie)\
**Post date:** [October 30, 2024, 9:22pm UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/2 "2024-10-30T21:22:02Z")

</div>

Oh, I actually wrote something similar recently! Unfortunately it’s on our private repo at work and would take a little bit of effort to get permission to open source. I wish I would have put it in ConcreteStructs instead, now! Maybe this or something like it should live there.

As for the implementation of the one I wrote, I actually went for something that operates on modules and searches through the defined structs for non-concreteness. You have to explicitly opt out like:

```julia
struct MyStruct{R<:Real}
    an_abstract_field::Vector{Real}
    another_abstract_field::AbstractVector{R}
    a_concrete_field::Vector{R}
end

# Allow any fields to be abstract...
@allow_abstract_fields MyStruct

# ...or only allow certain fields
@allow_abstract_fields MyStruct: an_abstract_field, another_abstract_field

```

Since many of the people touching our Julia code are new to the language or only rarely have to interact with it, having this be opt-out made the most sense as we could put checks in CI and people wouldn’t have to know to add checks for their structs. But I think for pedagogical purposes, having an opt-in approach instead makes a lot of sense.

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [October 30, 2024, 9:23pm UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/3 "2024-10-30T21:23:54Z")

</div>

I was thinking the `@check_concrete` macro could apply both to struct definitions or whole modules (got the idea from `@stable` in DispatchDoctor.jl). That way you get the fine-grained control if necessary, or you can go brute force.

---

<div class="post-metadata">

**Author:** ![jonniedie](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jonniedie/32/12842_2.png) [@jonniedie](https://discourse.julialang.org/u/jonniedie)\
**Post date:** [October 30, 2024, 9:29pm UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/4 "2024-10-30T21:29:02Z")

</div>

Oooh, interesting. So something like

```julia
@check_concrete module MyModule
...
end

```

that eagerly expands any `include`s and places the check on any structs that are defined? That would have made things a _lot_ easier than what I had to do to check after the module’s already been created.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [October 30, 2024, 9:57pm UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/5 "2024-10-30T21:57:00Z")

</div>

```julia
The following definitions will execute without error:
...
    @check_concrete struct GoodType2{T<:Real}
        x::Vector{T}
    end

    @check_concrete struct GoodType3{T<:Real,V<:AbstractVector{T}}
        x::V
    end

```

Since these also allow parametric types with abstract type parameters and possibly direct abstract type fields, would it be worth including a function to check fields of particular parametric types as well? Or is the idea that other reflection and type inference tools like `@code_warntype` would be enough to catch those, and this is intended as training wheels for beginners who want specialized performance but don’t quite grasp what type annotations do where yet?

The per-type macro should exist for per-type designation especially interactive use, but I imagine it’s possible for a beginner to want to do this check across a _file_. `@check_concrete_all begin ... end` could be put in each file, but I don’t like scattering boilerplate and I’d be concerned about niche “top-level” stuff cropping up. The per-module macro would be nice, but it doesn’t inherently transform what will be evaluated in `include` calls and doesn’t address scripts not encapsulated by modules. A function for tagging definitions with the macro can be put into `include`, and it could look more or less like this not-a-working example:

```julia
function checkmaybestruct(e)
    if !(e isa Expr && e.head == :struct)
         return e
  # missing module check
    else
         return :(@check_concrete $e)
    end
end

include(checkmaybestruct, "customtypes.jl")

```

Note that this doesn’t address module expressions, so the per-module macro would be helpful, gets a bit recursive. The per-module macro finding `include` calls in top-level control flow would be tougher, but maybe that’s not necessary for training wheels.

---

<div class="post-metadata">

**Author:** ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)\
**Post date:** [October 30, 2024, 10:18pm UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/6 "2024-10-30T22:18:13Z")

</div>

I would rather use that, didactically, interactively, not as code annotation. Like

```julia-repl
julia> @check_concrete MyModule

```

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [October 30, 2024, 10:23pm UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/7 "2024-10-30T22:23:37Z")

</div>

That looks like a check after `MyModule` was already evaluated, so wouldn’t a function be more straightforward `check_concrete_structs_in(MyModule)`? That would neatly get around dealing with unevaluated `include`s, but how reliable is it to find every type manually defined in that module, as opposed to those imported or referenced from other modules or automatically defined for closures? The latter falls under `names(mod; all=true)` and `parentmodule`:

```julia
julia> module A
         function foo(x)
           ()->x
         end
       end
Main.A

julia> typeof(A.foo(1))
Main.A.var"#1#2"{Int64}

julia> Symbol("#1#2") in names(A;all=true)
true

julia> parentmodule(typeof(A.foo(1)))
Main.A

```

---

<div class="post-metadata">

**Author:** ![jonniedie](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jonniedie/32/12842_2.png) [@jonniedie](https://discourse.julialang.org/u/jonniedie)\
**Post date:** [October 31, 2024, 12:12am UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/8 "2024-10-31T00:12:17Z")

</div>

> wouldn’t a function be more straightforward `check_concrete_structs_in(MyModule)` ?

That’s how I implemented it, but it just took a _lot_ of effort to get it working right and I’m still not 100% sure it’s always correct. Having access to the AST of the struct definition would have been pretty beneficial.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [October 31, 2024, 12:28am UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/9 "2024-10-31T00:28:24Z")

</div>

Worth mentioning that on the other hand, some evaluation is useful for figuring out when a type is concrete (`isconcretetype`), so I don’t expect a macro working on a module to figure everything out itself, rather transform to code that does. I also imagine that checking many type definitions in a module or file should emit warnings or append to a log instead of error at the first type.

---

<div class="post-metadata">

**Author:** ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)\
**Post date:** [October 31, 2024, 12:52am UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/10 "2024-10-31T00:52:24Z")

</div>

> [@Benny](#):
>
> `@check_concrete_all begin ... end` could be put in each file, but I don’t like scattering boilerplate and I’d be concerned about niche “top-level” stuff cropping up. The per-module macro would be nice, but it doesn’t inherently transform what will be evaluated in `include` calls and doesn’t address scripts not encapsulated by modules. A function for tagging definitions with the macro can be put into `include` […]

All these concerns have been solved in DispatchDoctor.jl, as mentioned by @gdalle. You should take a look, it’s quite well thought out.

---

<div class="post-metadata">

**Author:** ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)\
**Post date:** [October 31, 2024, 6:20am UTC](https://discourse.julialang.org/t/rfc-macro-for-checking-that-a-struct-is-concretely-typed/122017/11 "2024-10-31T06:20:25Z")

</div>

> [@Benny](#):
>
> ```julia
> The following definitions will execute without error:
> ...
> @check_concrete struct GoodType2{T<:Real}
> x::Vector{T}
> end
> 
> @check_concrete struct GoodType3{T<:Real,V<:AbstractVector{T}}
> x::V
> end
> 
> ```
> 
> Since these also allow parametric types with abstract type parameters and possibly direct abstract type fields, would it be worth including a function to check fields of particular parametric types as well? Or is the idea that other reflection and type inference tools like `@code_warntype` would be enough to catch those, and this is intended as training wheels for beginners who want specialized performance but don’t quite grasp what type annotations do where yet?

Rather the second one. If you construct a `GoodType2` with, say, a `x::Vector{AbstractFloat}`, this is not a problem in the struct definition, and it can be fixed without redefining `GoodType2` itself. On the other hand, if the type annotations or parameters are insufficient in `struct GoodType2`, there is nothing you can do but redefine it, and that is impossible for people who can’t modify the source code.

> [@lmiq](#):
>
> I would rather use that, didactically, interactively, not as code annotation. Like

That would be very handy, but as @jonniedie said, having access to the AST (i.e. doing this when the struct definition is parsed) makes things much easier. You would think that all it takes is checking `fieldtypes` for concreteness, but that’s not sufficient in the parametric case:

```julia
julia> struct A{T}
           x::Vector{T}
       end

julia> fieldtypes(A)
(Vector,)

julia> isconcretetype(Vector)
false

```

In this example you would have to check `fieldtpes(A{Float64})` instead of `fieldtypes(A)`. But in the general case, structs may have many type parameters, and it’s not obvious wihch combination should be checked for field concreteness. It’s not even obvious how to find a combination that is allowed by subtyping constraints (in the general case I suspect it’s NP-hard).

> [@danielwe](#):
>
> All these concerns have been solved in [DispatchDoctor.jl](https://juliahub.com/ui/Packages/General/DispatchDoctor), as mentioned by @gdalle. You should take a look, it’s quite well thought out.

Yeah I still need to look at that to figure out how to handle the `include`s. It may not be perfect at first.
