Considering options other than code generation

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.

In this case, the only variation between these functions are simple types: a constant and a string. In another situation, we may need to call different functions within the body. That is okay as well because functions are first-class in Julia, and so we could just pass around a reference of the function.

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.

In computer science terms, closure is a first-class function that captures variables from an enclosing environment. 

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset