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