Managing complexity with $define

Our constructor example has gotten fairly hard to read! Before we take it any further, let's refactor it to make it more clear what it's doing.

Here, we will use the $define and $defcond keywords to define new aliases for some of our longer expansions and conditions. This will make our intent more clear, and hopefully easier to read.

#![allow(unused)]
fn main() {
use derive_deftly::define_derive_deftly;
define_derive_deftly! {
   Constructor:

   // First, we define some aliases.

   // (1)
   ${define FUNC_NAME {
        ${if tmeta(constructor(newfn)) {
            ${tmeta(constructor(newfn)) as ident}
        } else {
            new
        }}
    }}

   // (2)
   ${defcond F_DFLT fmeta(constructor(default))}

   // (3)
   ${define F_INITIALIZER
       ${if F_DFLT {
          ::std::default::Default::default()
       } else {
          $fname
       }}
   }

  // Now that's out of the way, here is the template expansion:
  impl<$tgens> $ttype where $twheres {
     $tvis fn $FUNC_NAME(
       $(
          ${when not(F_DFLT)}  $fname: $ftype ,
        )
     ) -> Self {
          Self {
              $( $fname: $F_INITIALIZER , )
          }
      }
   }
}

use derive_deftly::Deftly;
#[derive(Deftly)]
#[derive_deftly(Constructor)]
struct Foo {
    #[deftly(constructor(default))]
    s: Vec<String>,
    n: u32,
}
}

In the example above, we've introduced some aliases for our trickier expressions, so that our template is easier to read.

At // (1) and // (3), we've used $define to introduce new expansions. The earlier one ($FUNC_NAME) is the name of our function; the later one ($F_INITIALIZER) is the expression that we use to initialize the current field.

At // (2), we've used $defcond to introduce a new condition, called $F_DFLT. It is true whenever we have been told to use the Default value for a the current field.

The $defcond and $define keywords don't expand to anything themselves; their only affect is to create the interpretation of the keywords or conditions they define.

NOTE: To avoid name conflicts, aliases created by $define and $defcond must be in ALL_CAPS.

As a general practice, it's a good idea to prefix field-specific aliases with F_ and variant-specific aliases with V_.

How does the scoping work here?

You might have noted that, in our example above, we define our new keywords and conditionals at the start of our template. In this context, there is no "current field"— but two of our definitions use fmeta, which requires the existence of a current field. How does this work?

In brief: the body of a definition is interpreted at the point where it is used. No matter where we put the ${define F_INITIALZER ...} in our template, it will use the value of ${fmeta ...} from the point where we actually use $F_INITIALIZER.