The first tool in your toolkit should always be functions. Macros are primarily used to introduce new syntax. A good rule of thumb for macro usage is provided in this comment on a different thread: “you should know exactly what your macro is converting things to, otherwise the macro has gone too far.” In other words, macros are for transforming syntax, and functions are for computations. So normally when you are solving a new problem, you should first figure out how to solve it with functions (and perhaps user defined types). Then, maybe you can consider introducing new syntax with a macro that uses your functions under the hood.
I don’t remember much about crystal structure, so let’s consider the following simplified problem. We start with a point in space (represented by a position vector) and we have two possible operations on it: translation and rotation. Translation is represented by adding a vector to the position vector. Rotation is represented by multiplying the position vector with a rotation matrix. Let’s write a helper function that creates a rotation matrix for a given rotation angle in degrees:
function rotation(angle)
[cosd(angle) -sind(angle);
sind(angle) cosd(angle)]
end
Julia is already pretty smart about vector and matrix algebra, so if we try to add a rotation to a transformation, it won’t let us, because you can’t add a vector to a matrix:
julia> R = rotation(30)
2×2 Array{Float64,2}:
0.866025 -0.5
0.5 0.866025
julia> T = [2, 3]
2-element Array{Int64,1}:
2
3
julia> R + T
ERROR: DimensionMismatch("dimensions must match")
Ok, so if we want to translate a point and then rotate it, how do we do it? Like this:
julia> v = [1, 1]
2-element Array{Int64,1}:
1
1
julia> R * (v + T)
2-element Array{Float64,1}:
0.598076211353316
4.964101615137754
Of course we can turn this into a function so that we can perform the same operation on any point:
julia> translate_then_rotate(v) = rotation(30) * (v + [2, 3])
translate_then_rotate (generic function with 1 method)
julia> translate_then_rotate([3, 5])
2-element Array{Float64,1}:
0.33012701892219276
9.428203230275509
If regular vectors and matrices are not enough to describe your crystal operators, and you need more complicated behavior from *
and +
, then you can create your own user-defined types and add new methods to *
and +
so that the arithmetic operators have special behavior for your custom types.
For example, if you wanted to print a more informative error message when you try to add a rotation to a transformation, you could use something like the following.
struct Rotation
angle::Float64
matrix::Matrix{Float64}
# Define an inner constructor. This guarantees that
# the matrix field contains a valid rotation matrix.
function Rotation(angle)
matrix = [cosd(angle) -sind(angle);
sind(angle) cosd(angle)]
new(angle, matrix)
end
end
struct Translation
t::Vector{Float64}
end
function Base.:+(::Rotation, ::Translation)
throw(ArgumentError("Rotations and translations cannot be added."))
end
Base.:+(t::Translation, r::Rotation) = r + t
Let’s try it out in the REPL:
julia> R = Rotation(30)
Rotation(30.0, [0.8660254037844386 -0.5; 0.5 0.8660254037844386])
julia> T = Translation([2, 3])
Translation([2.0, 3.0])
julia> R + T
ERROR: ArgumentError: Rotations and translations cannot be added.
julia> T + R
ERROR: ArgumentError: Rotations and translations cannot be added.
Hopefully that gives you some ideas to get started.