I’m pretty sure OP is going through a Medium article, which I cannot access as a non-member.
css_str
splits a ;
-delimited string into entries, then it splits those entries into key-value pairs in a dictionary:
julia> css"a:bc;d:ef" # doesn't handle spaces
Dict{Symbol, String} with 2 entries:
:a => "bc"
:d => "ef"
You can do the same thing with a function, and in the vast majority of practical cases, you’d want it done by a function because macros are more limited. The importance of macros is that they work at parse-time.
What is parse-time? The Metaprogramming docs explains it but I’ll sum it up quickly. Everything starts as source code, the text you type. The parser has to change that into data Julia can understand. It is here that you encounter syntax errors; the parser realized the source code isn’t valid Julia.
Parsing source code doesn’t make many data types. Most code turns into Symbol
and Expr
. Literal values for Int
, Float64
, and String
are also present; literal means you have to write the value directly in the source code. A macro’s inputs are limited to these types, though it’s acceptable to output any type. css_str
thus only works if you write something like css"a:bc;d:ef"
; if you had x="a:bc;d:ef"
, there is no way to pass the variable x
into css_str
because only the symbol :x
exists at parse-time.
So why use a macro if its inputs are limited? css_str
is only a demonstration, it’s not representative of most macros. The usual use of macros is to transform code, Expr
to Expr
; a macro call would be equivalent to you manually writing the output into the source code. After a macro is done, parse-time is over and Julia evaluates the output. If it’s a Symbol
or Expr
, it gets evaluated like any untransformed code you wrote; if it is or contains any other types, those types just pass through.
Here’s another demonstration (read: impractical) of a macro. Say I want a log5 for convenience, but there’s no built-in one. Simple, I’ll just use the log identity:
julia> log5(x) = log2(x)/log2(5)
Let’s look at what a call would be lowered to (a step before compiling):
julia> @code_lowered log5(5)
CodeInfo(
1 ─ %1 = Main.log2(x)
│ %2 = Main.log2(5)
│ %3 = %1 / %2
└── return %3
)
Hm, it’s doing log2(5)
, but we could do that in advance. But I don’t want make a const
global variable just for that. With a macro specially designed to work on Expr
interpolated with more types or non-literals, I can compute the value before inserting it right into the method Expr
:
julia> @eval log5(x) = log2(x)/$(log2(5))
And now the lowered code does:
julia> @code_lowered log5(5)
CodeInfo(
1 ─ %1 = Main.log2(x)
│ %2 = %1 / 2.321928094887362
└── return %2
)
Now for the impracticality, Julia’s compiler is smart enough to do function calls on constants or static parameters, including log2(5)
, at compile-time if inferred and simple, and most constants are used more than once so they are actually worth const
global variables. There are better examples of @eval
code generation in the docs.