Wednesday, February 22, 2012

Internal Templates as Special Functions

In a previous post, I had proposed expanding on the concept of multi-line string literals as a way to support DSLs and templates from within Dart. Here is an alternate proposal, using function bodies instead.

Proposal: Represent internal DSL templates as a special type of function. This is similar to Closure Templates, where templates are represented as a function-like structure with a formal parameter list.
Here is what the template definitions would look like:
Sql query2(int minAge) /*-{
        select id,firstName,lastName
        from person
        where age > ${minAge} and groupId=${groupId}
}-*/;
Html personTable(Person p) /*-{
        <table>
          <tr><td>First Name</td><td>${p.firstName}</td></tr>
          <tr><td>Last Name</td><td>${p.lastName}</td></tr>
          </tr>
        </table>
}-*/;
String personTable(Person p) /*-{
        First Name ${p.firstName}
        Last Name ${p.lastName}
}-*/;
I stole the syntax from GWT's JSNI. It seems to work pretty well there.
Then for each supported type DSL there would be two classes:
  1. Html: a class representing the generated object, i.e. the result of executing the template
  2. HtmlTemplate: defines the dart-templatized grammar for one particular DSL
Examples:

  HtmlTemplate creates Html
  SqlTemplate creates Sql
  StringTemplate creates String
  YamlTemplate creates Yaml

/// defines a dart-templatized DSL grammar
///   there will be one instance per template method
///   each DSL will have different rules regarding 
///   where ${ } expressions can be located
///
///   @type param T is the type of object that this 
///       template creates (i.e. Sql, Html, String)
interface Template<T>
  /// called once - to parse the template
  /// could potentially do nothing and just store the templateText
  /// but fail-fast would be preferred
  TemplateParseResult prepare(String templateText);
        
  /// called every time a template method is called
  T create(TemplateInvocationMirror args)
        
  /// optional. used by tools ???
  /// it would be nice if each ide/editor/tool didn't have to reinvent
  /// everything from scratch
  TemplateGrammar get grammar;
}
/// provides detailed parse error info: line #, column #
interface TemplateParseResult{
}


Making Tool Support Easier
It would nice if tool support for DSL templates is possible. It would be nicer, if tool support for DSL templates actually existed. Perhaps the TemplateGrammar class could provide enough information, so that tools (like IDEs) would not require 400 years in order to start supporting auto-completion, syntax high-lighting, etc.



/// optional. used by tools ???
/// it would be nice if each ide/editor/tool didn't have to reinvent
/// everything from scratch
/// one instance per DSL

interface TemplateGrammar{


}

1 comment:

Eric Leese said...

That's pretty similar to what I'm thinking. But I'm not sure about the functions thing -- I think if you do that in a lot of cases the function will only be called once and what you'll end up with is a lot of functions inside functions that do not improve the readability. And the => syntax makes it easy to wrap a template with a function if that is what you want.

Also, if this is a proposal for DART, you've violated the optional typing principle by making the behavior of a function be affected by its declared return type! That's why I had proposed a constructor syntax for this.

Which language is in charge of understanding the syntax of string interpolation and the expressions that live within it? If ${} contains expressions in the host language, you want the host language to parse it, so the constructor for the template needs to be a little more complicated than taking a String.

Now the issue is that if you add $for and $if to the string interpolation language as I proposed last post, it gets a lot harder to precompile the templates! With simple string interpolation, you can do your regular grammar with INTERPOLATED_VALUE as one extra predefined token you need to handle in your grammar. Add control flow and... I think that's still what you want to do, though. Depending on where the control flow breaks things up it's possible that 1) You can't tokenize the string until you know the control flow path 2) You can pre-tokenize the string but you can't say anything about the expression stack at the beginning/end of control flow blocks 3) The expression stack at the beginning of control flow blocks can be determined 4) There's no control flow to worry about or 5) No string interpolation is used. So providing a parser generator tool is key because then we can take care of precompiling as much as can be for a given template instance and doing the rest at instantiation time, rather than having DSL writers figure out the various levels of precompilation they want to handle or worse try to figure out all the ways control flow can interact with their grammar and try to make it a part of it.