I am fairly new to Julia, so apologies for any misunderstandings of the language I may have. I have mostly used Python recently, and made heavy use of SymPy and its code generation features, and it seems like Julia with its metaprogramming features is built for writing code in exactly the kind of style I like.
In particular, I want to construct block matrices in Julia from a set of smaller building blocks with some different operations in between. For debugging purposes and because the various intermediate matrices are used in other calculations, I want to keep them as expressions containing variables, such that I can quickly loop over and test different different inputs, without wrapping everything in a function.
Now, for a minimal case study, say I have two expressions mat1 = :a
and mat2 = :b
that I want to combine to form a new, third expression:
mat3 = :($mat1 + $mat2)
The above method works fine until I modify mat1
and mat2
, in which case I have to re-evaluate mat3
in order to reflect this update. This is caused, I presume, by the fact that $mat1 + $mat2
is not passing mat1
and mat2
by reference, but rather interpolates the expressions inside at the time of evaluation of that line. The behaviour I want to achieve is that mat1
and mat2
are not inserted until I call eval(mat3)
, preferably with minimal boilerplate.
Is it possible to achieve this in a handy syntax?
mat3
will reflect the mutation of mat1
and mat2
, but not rebinding of mat1
and mat2
. It is important to understand the distinction between mutation and rebinding.
Mutation occurs when the data of an object is modified. Note that this does not affect any names, only objects. This can manifest in many ways, including functions like push!
and assignment syntax with a complex left-hand side, like A[1] = 5
.
For example, all of the following are examples of mutation:
A = [1, 2, 3]
A[1] = 4
The name A is unchanged; A still points to the same object. The object that A
represents is modified.
A = :(f(x))
A.args[1] = :g
The name A is unchanged; A still points to the same object. The object that A
represents is modified.
mat1 = :(f(x))
mat2 = :(f(y))
mat3 = :($mat1 + $mat2)
mat1.args[1] = :g
The name mat1
is unchanged; it still points to the same object. That object is modified. mat3
references that same object also, and because it's been modified, it will reflect the changes. Indeed, now mat3
contains :(g(x) + f(y))
.
(also known as assignment)
Rebinding occurs when no object data is modified but the target of a name is changed to that of a different object. This is indicated by a simple =
assignment, with the left hand side being the thing rebound.
x = 2
x = 3
Here x
is being rebound from the object 2
to the object 3
. We are not changing the object 2
. In fact, because 2
is an immutable object, it is not allowed to mutate the object 2
. Instead, the reason the observable value of x
has changed is because it references a different object now: 3
.
A = [1, 2, 3]
A = [4, 2, 3]
Once again here we are not mutating the vector A
; we're creating a new vector and now A
references this new vector. Distinguishing between mutation and rebinding is important. Once again, mutation acts on objects, and rebinding acts on names.
mat1 = :x
mat2 = :y
mat3 = :($mat1 + $mat2)
mat1 = :z
Note here that the simple assignment does not mutate the object :x
that mat1
references; it simply rebinds mat1
to the different object :z
. This means that mat3
, which contains the object :x
, will not be affected.
Note that Symbol
is an immutable type, so you cannot mutate it. Thus it is impossible to do what you're proposing.
A better way to do what you're proposing is to use a function instead of a single expression. A function can be called multiple times, producing different objects.
mat1 = :x
mat2 = :y
mat3() = :($mat1 + $mat2) # function definition
mat3() # :(x + y)
mat1 = :z
mat3() # :(z + y)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With