Why is `Expr` mutable?

Expr does not have object identity have field equality, i.e.

julia> Expr(:call, println) == Expr(:call, println)
true

I was surprised to learn that Expr nevertheless is mutable. I can easily imagine why Expr does not have object identity (“Oh, no, I do not mean to call this println method without arguments in the current module, I instead want to call that println method without arguments in the current module” does not really make sense).

The problem with being mutable is that an immutable struct with an Expr field now surprisingly gets object identity loses field equality:

struct MyExpr
    expr::Expr
end

julia> :block |> Expr |> MyExpr == :block |> Expr |> MyExpr
false

Note, that this is in contrast to Symbol, which I had expected to being implemented completely parallel to Expr, but which is immutable and therefore no surprising object identity pops up field equality gets lost:

struct MySymbol
    symbol::Symbol
end

julia> MySymbol(:a) == MySymbol(:a)
true

Semantically, I therefore do not understand, why Expr is mutable.

Looking at its memory structure presented in Julia (I haven’t checked the C implementation)

julia> using About

julia> Expr |> about
Concrete DataType defined in Core, 
  Expr <: Any

Struct with 2 fields:
• head  Symbol     
• args  Vector{Any}

julia> sizeof(Expr)
16

it looks like the args can be changed anyway as they are part of the mutable Array. Only when modifying the head, which might not happen in isolation too often anyway, a new object were to be constructed if Expr were immutable. But I think this would be as cheap as it could get for non-trivial immutable objects.

From a performance point of view, I therefore do neither understand, why Expr is mutable.

So why is Expr mutable?

2 Likes

=== is the comparison for object identity, and it’ll be false for your first example.

3 Likes

One reason might be that Expr existed long before immutable structs were implemented in the language.

2 Likes

Thanks for the correction! Do you know of a better term for == being true although === might be false?

julia> a = [1,2,3]; b=[1,2,3];
julia> a == b
true
julia> a === b
false

julia> a = 1; b =1
1
julia> a == b
true
julia> a === b
true

julia> struct Foo
       x::Int64
       end
julia> a = Foo(1); b = Foo(1);
julia> a == b
true
julia> a === b
true

julia> mutable struct Goo
       x::Int64
       end
julia> a = Goo(1); b = Goo(1);
julia> a == b
false
julia> a === b
false

Equal, but not identical?

1 Like

If a === b we say that a and b are “egal”, whereas if a == b we say they are “equal”.

Egality is a “stronger” condition

1 Like

Thanks to all the suggestions. I needed it as a noun and changed the post to use field equality. Sorry for the confusion with the terminology.

Back to the original question: Does anyone know the reason why Expr is mutable? I think @GunnarFarneback has a valid point, but I guess there would have been enough time before Julia 1.0 to make Expr immutable.

I can only guess, but my inkling is that it is some combination of one or more of the following:

  • Having Expr immutable doesn’t provide much gains in practice.
  • Many existing macros would break from the change.
  • While args is indeed mutable, it’s nevertheless easier to be able to replace it than having to do empty! + append!.

Potentially there could also be complications in the early parts of bootstrapping, but there I have zero insights.

3 Likes

9 posts were split to a new topic: How is Symbol special?

My guess is that it is mostly historical: the Expr type dates to the earliest days of Julia, before immutable struct types existed in the language, and since they aren’t so performance critical it wasn’t worth the potential breakage to make them immutable once this became possible.

2 Likes