DSL Patterns - Multi File DSLs

time to read 5 min | 803 words

One of the common misconceptions about a DSL is that it to think about each DSL script independently. As a simple example, let us go back to the routing DSL, we may have a rule like this:

priority 10
when
msg is NewOrder and msg.Amount > 10000:
dispatch_to "orders@strategic"

And our focus is mainly on the actual language and syntax that we want to get. This is a mistake, because we aren't considering the environment in which the DSL lives. In the same sense that we rarely consider a code file independently, we should not consider each script independently.

With the routing DSL (and yes, I am stretching the example), we may need to perform additional actions, rather than just dispatch the message. For example, we may want to log all strategic messages. As you can see, we can easily add this to the DSL:

priority 10
when msg is NewOrder and msg.Amount > 10000:
     log "strategic messages" , msg
     dispatch_to "orders@strategic"

However, this is a violation of SoC, and we care about such things with a DSL just as much as we care about them with code. So we can do it like this, leave the original snippet and add another, like this:

when destination.Contains("strategic"):
	log "strategic messages", msg

Now the behavior of the system is split across several files, and it is the responsibility of the DSL engine to deal with this appropriately. One way to arrange this would be this folder structure:

  • /routing_scripts
    • /routes
      • /orders
        • dispatch_big_new_orders_to_strategic_customers_handling
        • dispatch_standard_new_orders_to_normal_customers_handling
    • /behaviors
      • /after
        • /log_strategic_messages
        • /dispatch_to_error_queue_if_not_dispatched

The DSL engine can tell (from the message) that it needs to execute only the routing rules in /routes/orders, and it can execute the before and after actions without getting the routing scripts tied to different concerns.

If you want to be a stickler, we are actually dealing with two dialects that are bound to the same DSL engine. In this case, they are very similar to one another, but that doesn't have to be the case.

Multi file DSL doesn't have to be just about combining different DSLs together, they are also about binding different scripts. Let us look at another possible folder structure:

  • /routing_scripts
    • /routes
      • /orders
        • dispatch_big_new_orders_to_strategic_customers_handling
      • convention_based_dispatching
    • dispatch_to_error_if_not_dispatched

    In this case, the DSL is based around the idea of executing things in reverse depth order. That is, when a message arrives, we try to match it to the deepest scope possible (in this case, handling strategic customers), and we go up until we reach the root.

    This is still, however, just another way of bringing different scripts together. Albeit in a fairly interesting ways.

    Let us consider a completely different example, order management. We might have something like this:

    // this is part of the rules for processing an order
    when order.Total > 1000:
    	large_order
    
    // this is the large_order script
    order.TaxRate = 0.10
    if user.Location in (complex_taxation.Locations):
    	complex_taxation.Handle(order)
    
    // this is the complex_taxation/mars script
    if amount % 13 == 0:
    	return 0.0
    return amount * 0.04

    Admittedly not the best example as far as business logic goes, but you can see the interaction of three different scripts and how they rely on one another to split the complexity of the system up. It also means that changing the taxation rules on mars is not going to touch how we are going to handle an order, which is good (SoC again).

    In this case, I don't think that there is any doubt that those scripts are truly part of a multi file DSL.

    Again, when designing and building a DSL, thinking about just a single file is the easiest way to go, but you ought to consider the entire environment when you make such decisions. A language that does not support SoC is bound to get extremely brittle very fast.