Text-based Macro System for F#

One feature I’ve seen repeatedly people wishing is a macro system, so I went ahead and tweaked the compiler to add a text-based macro system. I’m warning here and now: this is only a prototype, it is imperfect and definitely incomplete. The current version uses in-place text replacement on compilation time, using precompiled macros (functions marked with a special attribute).

Using Macros

Before you can use a macro, you need to tell the compiler in which .NET assembly it can be found. This is done similarly to adding references, but using the –macro-provider flag instead of the –reference flag (-m for short, instead of -r for short). The compiler will search in all classes/modules marked as MacroModule for static methods/functions marked as Macro which match a specific signature (MacroInput -> MacroOutput). Macro usage syntax is <<name {code}}>>. The name can be any legal F# identifier and the code can be any string which does not contain two right curly-brackets immediately adjacent (the curly-brackets can be escaped: }}} goes to }}, }}}} goes to }}} etc.).

There are currently 4 demo macros (I have some others for specific uses I needed):

  • LambdaMacro – typed λ-calculus (e.g. <<(λ {{fgy.f(λz.gzy)y}}) : ((‘a –> ‘b) –> ‘c –> ‘d) –> (‘a –> ‘c –> ‘b) –> ‘c –> ‘d>>)
  • InlineMacros – C-style replacement macros. Define a macro using <<def {{add(a, b) –> ((a) + (b))}}>> or <<def {{LENGTH –> 8000}}>> and then use the macro <<add {{2 * 3, 4 * 5}}>> or <<LENGTH {{}}>>. It is possible to escape the comma (,). This macro is using the MacroAPI which allows macros to see which macros are available to register additional macros.
  • XmlLiterals – XML literals similar to Opa’s XML literals (that is, closing a tag is with </> instead of </name>): <<xml {{<a href="http://fpish.net/org/~<~"c" + string 4 + "fs"~>~">Here is ~<~xml {{{<i>F#</>}}}~>~!</>}}>> (see how the inner XML literal has escaped curly-brackets). FSI responds to this input: <<val it : System.Xml.Linq.XElement = <a href="http://fpish.net/org/c4fs"> Here is <i> F# </i> ! </a> {…}>>.
  • LaTeXFormulae – support for basic LaTeX input. To be honest, this is my favourite macro, and it helped me convince several people that F# is awesome. Here are some examples:

LaTeX {{\frac{f\left(x+h\right)-f\left(x\right)}h}}

LaTeX {{\sum_{i=1}^x{\binom{x}{i}}} }}

LaTeX {{\sum_{i\in a}{\left(\binom{n^\left(i-1\right)}i+\left|\left\{1,\frac23,3\right\}\right|\right)} }}

Writing Macros

Writing macros is simple, but there are some catches (at least in the current prototype). In general, macros have two parts – parsing and code generating. I usually use FParsec and manually generate F# code, but it is also possible to parse with FsLex/FsYacc or any other parser, and it is also possible to create a quotation printer and write a parser which returns an F# quotation. Here is all you’ll need to know in order to write a macro:

  • The reference list must be updated to use the modified FSharp.Core.
  • The class/module containing the macro/macros must be marked with MacroModuleAttribute (from Microsoft.FSharp.Compiler.MacroProviders).
  • The macro itself must be marked with MacroAttribute (from Microsoft.FSharp.Compiler.MacroProviders).
  • The macro must be a function from MacroInput to MacroOutput (both from Microsoft.FSharp.Compiler.MacroProviders).
  • All non-trivial references (trivial references – those are references which FSC/FSI use) must be statically linked (this will be changed in future versions; I know how, just takes time).
  • Generated code should not produce any warnings (it is just terrible ugly).
  • Since the code replacement is in-place, the generated code should/must have the same line count as the macro usage (so the warnings/errors of the following code will have the correct positions) – this is no longer correct, see update below.
  • Macros can generate warnings (there is a function for that in the MacroInput) and can get the text position (also in MacroInput).
  • MacroOutput is a discriminated union of MSuccess (returning a string containing the generated code) and MFailure (returning the error message).

Conclusions

I believe macros can improve greatly the usability of F#, including sugar for lenses (see here), type classes (see here for an idea) and joinads (see here). In addition to being a great feature for F# 2, F# macros will complete the F# 3 type providers. While the type providers are useful for generating many classes on-demand from files, web and other resources, the F# macros give you similar features in-line. This improves readability if you have many short/medium-length one-time uses of specific macros.

It is possible to tell F# to use the custom F# version, but the design-time error reports (red-blue squiggly lines) always use the built-in F# version. It seems that without the F# team’s help, there is no good solution for this problem (the bad solution being creating custom F# for VS Shell).

 

Files can be downloaded from here.

Update: A new version (2.1.2.1033) can be downloaded from here with updated demos. The important change is that I tweaked the lexer so that the code generated by the macros will not affect the positions of the following tokens (making the macro writing easier and the error messages better). In the 9-items list, this is item #7.

This entry was posted on Saturday, December 3rd, 2011 at 4:54 PM and is filed under Default. You can follow any responses to this entry through the RSS 2.0 feed. You can skip to the end and leave a response. Pinging is currently not allowed.

Leave a Reply