DRY Your Code with Macros

Crystal’s original inspiration, Ruby, is a master of runtime introspection and manipulation of code—also called metaprogramming. Metaprogramming is the secret to Ruby on Rails’ sophistication, but Ruby has no macros. Crystal is a compiled language, so it doesn’t have an eval method to create new code in runtime. It has to take a different path, a competent system of macros to build code at compile time, and that will go a long way.

When you’re writing code, sometimes you’ll find yourself writing methods that are near duplicates of each other, only differing in name or parameters. In these cases, it might help if you could generate all this code automatically using one macro version of the method.

A macro is a function that gets called while code is compiled. The output of the macro is more code, which gets compiled. Macros let you do more with less code.

Less code (usually) means fewer bugs. Don’t repeat yourself—apply the DRY principle!

For example, let’s see how we could implement a macro that returns the value of instance variables. We’ll start from a simple Mineral class with attributes name and hardness, pretending we don’t know about getter:

 class​ Mineral
 def​ ​initialize​(@name : String, @hardness : Float64)
 end
 
 def​ ​name
  @name
 end
 
 def​ ​hardness
  @hardness
 end
 end
 
 min1 = Mineral.​new​(​"gold"​, 2.5)
 "​​#{​min1.​name​​}​​ - ​​#{​min1.​hardness​​}​​"​ ​# => "gold - 2.5"

Code is duplicated: for each attribute, we have a method of that name to return its value.

But we want something like this to use a new macro called get, right?

 class​ Mineral
 def​ ​initialize​(@name : String, @hardness : Float64)
 end
 
  get name
  get hardness
 end

or even:

 get name, hardness

Let’s do this in steps:

1) First, copy the name method into a macro, get, like this:

 macro get
 def​ ​name
  @name
 end
 end
 
 class​ Mineral
 def​ ​initialize​(@name : String, @hardness : Float64)
 end
 
  get
 
 def​ ​hardness
  @hardness
 end
 end
 
 min1 = Mineral.​new​(​"gold"​, 2.5)
 "​​#{​min1.​name​​}​​ - ​​#{​min1.​hardness​​}​​"​ ​# => "gold - 2.5"

The code still works: the macro, get, creates code for the method, name. A macro is defined like an ordinary method. But instead of def, the keyword macro is used.

2) Because we want this for every attribute, we must generalize the code:

 macro get(prop)
 def​ {{prop}}
 @​{{prop}}
 end
 end
 
 class​ Mineral
 def​ ​initialize​(@name : String, @hardness : Float64)
 end
 
  get name
  get hardness
 end
 
 min1 = Mineral.​new​(​"gold"​, 2.5)
 "​​#{​min1.​name​​}​​ - ​​#{​min1.​hardness​​}​​"​ ​# => "gold - 2.5"

The macro get now takes a parameter, prop, so we can use it for every attribute. The body of a macro often contains {{ }} expressions. These are expanded when code is generated from the macro at compile-time: every expression inside {{ }} is substituted in the generated code. So now we can use get for every attribute.

3) How can we reduce the code even more to get name, hardness? Because we don’t know how many attributes there are, we use a splat * (see Using the Splat Argument *). To loop over the attributes, we can use the following for in syntax:

 {​% for ​prop ​in​ props ​%}
  # code
 {% end %}

The complete version looks like this:

 macro get(*props)
  {​% for ​prop ​in​ props ​%}
  def {{prop}​}
 @​{{prop}}
 end
  {​% end ​%}
 end
 
 class​ Mineral
 def​ ​initialize​(@name : String, @hardness : Float64)
 end
 
  get name, hardness
 end
 
 min1 = Mineral.​new​(​"gold"​, 2.5)
 "​​#{​min1.​name​​}​​ - ​​#{​min1.​hardness​​}​​"​ ​# => "gold - 2.5"

The Crystal language includes many powerful built-in macros, such as getter, setter, and property, in a class definition. These aren’t keywords. They are macros defined in class Object. There’s even a record macro that can generate an entire struct definition for you:

 record Mineral, name : String, hardness : Float64
 
 min1 = Mineral.​new​(​"gold"​, 2.5)
 "​​#{​min1.​name​​}​​ - ​​#{​min1.​hardness​​}​​"​ ​# => "gold - 2.5"

Macros are a great way for you to extend the language, even for writing DSLs (Domain Specific Languages).

Like the {% for in %} construct, there’s a {% if %} {% else %}. You can use both outside of a macro definition as well. Inside a macro, you can access the current instance type with the special instance variable @type. Macros can live inside modules or classes. They can call each other, and a macro can even call itself recursively. Be careful, though—you need to define macros before you use them.

How do macros work? The compiler takes a few extra steps to generate the actual executable code. In the step that processes the Abstract Syntax Tree (AST), code using the macro syntax works on these AST nodes. These expand into valid Crystal code, which then compiles as usual. By hooking into the compilation process, you can do some sophisticated things, and it doesn’t slow down runtime performance like it does in Ruby!

Your Turn 1

def_method: Make a macro, define_method, that takes a method name, mname, and a body to construct that method. Test it by producing the code for a method, greets, that prints “Hi,” and a method, add, that returns 1 + 2.

Hooking Up Macros

You might know from Ruby that you can generate code in runtime when a called method can’t be found. You’d do this by defining a method_missing in the class. In Crystal, you can do something very similar with a macro, but here you generate the method at compile-time:

 class​ Mineral
  getter name, hardness
 
 def​ ​initialize​(@name : String, @hardness : Float64)
 end
 
  macro method_missing(call)
  print ​"Unknown method: "​, {{call.​name​.​stringify​}},
 " with "​, {{call.​args​.​size​}}, ​" argument(s): "​,
  {{call.​args​}}, ​' '
 end
 end
 
 min1 = Mineral.​new​(​"gold"​, 2.5)
 min1.​alien_planet?​(42)
 # => Unknown method: alien_planet? with 1 argument(s): [42]

In the preceding example, the method alien_planet? doesn’t exist in class Mineral. Normally, this would cause a compile error:

 undefined method 'alien_planet?' for Mineral

With this macro, method_missing, in place, we have access to the method’s name and arguments, coding something more useful than just printing them out.

In the same way, you can define the following macros, which are invoked at compile-time:

  • inherited: When a subclass is defined, @type is the class that inherits.
  • included: When a module is included, @type is the class that includes.
  • extended: When a module is extended, @type is the class that extends.

Using these, you can program at a meta-level.

While macros are powerful, coding with macros is a lot more complicated. So, as a rule, don’t use macros! If you really think you need a macro, first write the code without it, and check that you have duplicated code. If you do, then eliminate that by writing a macro. As you get deeper into macros, you will find more nuance and power in the docs.[47]

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

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