Please jump to this post below if you do not want to read my implementation and want to code it yourself!
Hi, community,
I am trying to write a beautiful DSL so the user can feel that she is writing mathematical equations as in a piece of paper. So far, I have two approaches in my head and I want to discuss them with you. Please, stay tuned.
I have seen that sometimes there is a model
defined that is given as input for macros. For example, take a look at the JuMP.jl package. Following that approach and using a really simple example where two scalar variables are defined (more complex examples include the definition of vectors, matrices, functions, etc), I could do something like this:
model = Model()
@var(model, x ,1.) # similar to @var model x = 1.
@var(model, y, x + 1.) # similar to @var model y = x + 1.
where Model
is described as:
const Value = Union{Number, AbstractArray{T} where T<:Number}
const ValueOrNothing = Union{Value, Nothing}
mutable struct Variable
name :: Symbol
value :: ValueOrNothing
end
struct Model
vars :: Dict{Symbol, Variable}
end
Letâs take a look at the source code of @var
:
const GeneralExpr = Union{Symbol,Expr,Float64,Int64}
"""
Performs a variable definition or assignment following a pattern of the type
{ Symbol | Expr } = { Literal | Symbol | Expr }, e.g.:
x = 1. # Symbol = Literal
x = y # Symbol = Symbol
x = y + z # Symbol = Expr
v = w # Symbol = Symbol
v = [i for i in 1:3] # Symbol = Expr
v[1] = 1. # Expr = Literal
v[1] = a # Expr = Symbol
v[1:3] = [1, 2, 3] # Expr = Expr
v[i:j] = [z for z in i:j] # Expr = Expr
where `y`, `z`, `w`, `a`, `i` and `j` are previously defined variables.
"""
macro var(model::Symbol, lhs::Union{Symbol,Expr}, rhs::GeneralExpr)
# checks
lhs isa Expr &&
lhs.head != :ref &&
error("unexpected left hand side '$lhs' for assignment.")
# get the variable name if assignment is of type v[i] = x
name = lhs isa Symbol ? lhs : lhs.args[1]
return quote
# checks if provided model is of Model type
_valid_model($(esc(model)), $(quot(model)))
# check if variable already exists in model. If it does not, create it.
if getobject(vardict($(esc(model))), $(quot(name))) == nothing
define_variable($(esc(model)), $(quot(name)))
end
# build a function Expr (explained above!)
assignment_ex = build_assignment(
$(quot(lhs)),
$(quot(rhs)),
$(esc(model)),
)
# generate a function that performs the assignment
instruction_assignment = $__module__.eval(assignment_ex)
# perform the assignment
instruction_assignment ()
end
end
If we expand the first macro call @var(model, x, 1.)
we get:
quote
umc._valid_model(model, :model)
if umc.getobject(umc.vardict(model), :x) == umc.nothing
umc.define_variable(container, :x)
end
octopus = umc.build_assignment(:x, 1.0, model)
dotterel = (Main).eval(octopus)
dotterel()
end
where octopus
is actually a expression of a Function that performs the assigment of a variable:
quote
octopus::Model = model -> begin
x = (octopus.vars[:x]).value
(octopus.vars[:x]).value = 1.0
end
end
This expression is evaluated and transformed into a function pointer dotterel
, which is then called. Since the function has a model
as a default argument of type Model, there is no need to provide any argument for the function call at dotterel()
. However, this argument is not const
and it is also a struct with a dictionary of elements of Variable
type, with not type-stable value
. Could someone help me with this?
For the second macro call, @var(model, y, x + 1.)
we get:
octopus::Model = model-> begin
y = (octopus.variables[:y]).value
x = (octopus.variables[:x]).value
(octopus.variables[:y]).value = x + 1.0
end
So we can use the value of x
to perform the assignment.
The other approach would be completely different and given by:
@input begin
@var x = 1. # or just x = 1.
@var y = x + 1. # or just y = x + 1.
end
Letâs leave this approach for later!
Thank you very much!