Throughout this section, we have been focusing on code generation techniques. The premise is that we can easily add a new function that works just like an existing one but a little differently. In practice, code generation is not the only option we have on hand.
Let's continue our discussion with the same example. As we recall, we wanted to add the warning! and error! functions after defining the logic for info!. If we take a step back, we can generalize the info! function and make it handle different logging levels. This can be done as follows:
function logme!(level, label, logger::Logger, args...)
if logger.level <= level
let io = logger.handle
print(io, trunc(now(), Dates.Second), label)
for (idx, arg) in enumerate(args)
idx > 0 && print(io, " ")
print(io, arg)
end
println(io)
flush(io)
end
end
end
The logme! function looks exactly like info! before, except that it takes two extra arguments: level and label. These variables are taken and used in the body of the function. Now we can define all three logging functions as follows:
info! (logger::Logger, msg...) = logme!(INFO, " [INFO] ", logger, msg...)
warning!(logger::Logger, msg...) = logme!(WARNING, " [WARNING] ", logger, msg...)
error! (logger::Logger, msg...) = logme!(ERROR, " [ERROR] ", logger, msg...)
As we can see, we have solved the original problem using a regular structured programming technique, and we have minimized as much repetitive code as possible.
Can we do better? Yes. The code can be simplified a little more using closure technique. To illustrate the concept, let's define a new make_log_func function as follows:
function make_log_func(level, label)
(logger::Logger, args...) -> begin
if logger.level <= level
let io = logger.handle
print(io, trunc(now(), Dates.Second), " [", label, "] ")
for (idx, arg) in enumerate(args)
idx > 0 && print(io, " ")
print(io, arg)
end
println(io)
flush(io)
end
end
end
end
This function takes the level and label arguments and returns an anonymous function that contains the main logging logic. The level and label arguments are captured in a closure and used inside the anonymous function. So, we can now define the logging functions more easily as follows:
info! = make_log_func(INFO, "INFO")
warning! = make_log_func(WARNING, "WARNING")
error! = make_log_func(ERROR, "ERROR")
So, three anonymous functions are defined here: info!, warning!, and error! and they all work equally well.
Technically speaking, there is a non-trivial difference between the structured programming solution and closure. The former technique defines generic functions that are named functions within the module that can be extended. In contrast, anonymous functions are unique and cannot be extended.
In this section, we have learned how to do code generation in Julia and how to debug this code. We have also discussed how to restructure code to achieve the same effect without having to use code generation technique. Both options are available.
Next, we will discuss DSLs, which is a technique for building syntax for specific domain usage, thereby making the code much easier to read and write.