4
votes

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?

1
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.Fengyang Wang

1 Answers

9
votes

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

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)).

Rebinding

(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)