简体   繁体   中英

f# Computation expressions for code generation

While one finds a few examples on how to do compositional recursive descent Parsers with f# computation expressions, I tried to use them for the opposite. To create easily readable code to generate (c++) source files from some XML data. Yet, I am stuck and it would be much appreciated if the community could help me find my misunderstandings. For the public benefit, my hope is that soon this post will Showcase how to do code Generators in a cool way by means of f# computation expressions, monadic style.

Here is how far I got so far (simplified, omitting the Input data for the Generation for the purpose of this question):

// in my full fledged application, State type also contains the Input data, used for generating code.
type State() = 
    let builder = new System.Text.StringBuilder()
    let mutable indentLevel : int = 0
    member this.Result() = builder.ToString()
    member this.Emit (s : string) : unit = builder.Append( s )
    // ... Methods allowing to do the indenting right, using indentLevel. And adding Output to the builder instance.
    member this.Indent() = indentLevel <- indentLevel + 1
    member this.Exdent() = indentLevel <- indentLevel - 1
// The return value of the Formatters is State only to allow for |> pipelining.
type Formatter = State -> State
type FormatterBuilder() = 
    // Q: Bind() Kind of Looks wrong - should it be a generic, taking one generic first Parameter? See Class function below.
    member this.Bind (state,formatter) = formatter state
    member this.Return state = state              // Q: Not sure if this is the way to go. Maybe some Lambda here?!

let format = new FormatterBuilder()

// Q: Now Comes the part I am stuck in!
// I had the idea to have a "Block" function which 
// outputs the "{", increases the indent Level, 
// invokes the formatters for the Content of the block, 
// then reduces the indent Level, then Closes "}". 
// But I have no idea how to write this.
// Here my feeble attempt, not even sure which Parameters this function should take.
let rec Block (formatters : Formatter list) (state : State) : State =
    format 
        {
            state.EmitLine("{") // do I Need a "do!" here?
            state.Indent()
            formatters |> List.iter (fun f -> do! f state) // Q: "state" is not really propagated. How to do this better?
            state.Exdent()
            state.EmitLine "}"
        }
// Functions with "Get" prefix are not shown here. They are supposed to get the Information
// from the Input, stored in State class, which is also not shown here.
let rec Namespace (state : State) : State =
    format
        {
             state.EmitLine(GetNameSpace state)
        }
let rec Class (classNode : XmlNode) (state : State) : State =
     Format
        { 
             do! TemplateDecl classNode state   // TemplateDecl function not shown in sample code
             do! ClassDecl classNode state
             do! Block [ NestedTypes classNode; Variables classNode; // ... ] // just to give the idea. Q: the list seems wrong here - how to do it better? 
        }
let GenerateCode() : string = 
     let state = new State()
     format
         {
             do! Namespace state    // Q: Is there a way to get rid of the passing of state here?
             do! Block 
                [   // Q: Maybe a Seq is better than a list here?
                 for c in State.Classes do // Q: requires override of a few functions in Builder class, I guess?!
                  do! Class c state
                ]
         }    
     state.Result()

Obviously the above code at best only shows what I try to achieve. My research did not yield any good examples on how to use computation expressions. Many examples I found stop at showing how the builder is declared or a bit later, but fail to Showcase how to actually write the final expressions.

So, if someone finds the time to post a real sample which does what my gibberish code above tries to do, it would be most instructive and fill a gap of what can be found on the Internet regarding this (at least for me) confusing aspect of f# programming.

In my code sample above, I also fail to see what I get from the builder monad in the first place. The formatter code is not looking cleaner compared to a non-monadic implementation.

It would be great, if someone added the signatures and types to Parameters in answer posts; at least for me it is much more understandable compared to the "let-the-compiler-find-the-types" style.

OK, as I mentioned in the comments, this is a functional-style solution I've been using for a while with good success, though it's not purely -functional and it just uses some simple functions instead of a computation expression.

First, the code: grab CodeGen.fs from my facio repository. If you want to see how I've used those functions in practice, look at FSharpLex/Backend.Fslex.fs and FSharpYacc/Backend.Fsyacc.fs .

So, here're my reasons for implementing my code-generation as such:

  • The functions I've defined in the IndentedTextWriter module are very lightweight and (IMO) easy-to-use. If you do decide to use my functions in your own code, you could forgo the [<RequireQualifiedAccess>] attribute on the module or change it to [<AutoOpen>] to reduce the noise a bit.

  • Instead of implementing a bunch of code to manage the indentation level and emit the indentation strings into the underlying StringBuilder , I prefer to use System.CodeDom.Compiler.IndentedTextWriter since it handles all of that for you, and it's also an instance of TextWriter so you can use it with functions like fprintf and fprintfn .

    Bonus: IndentedTextWriter is included in System.dll , and since you'll almost certainly be referencing that anyway, you don't even need to add an additional reference to use it!

  • IndentedTextWriter just wraps another instance of TextWriter , so the code you write with it (eg, using my functions in CodeGen.fs ) isn't tied to a specific "target". In other words, you can easily modify it to write to a StringBuilder (with StringWriter ), a file on disk (with StreamWriter ), and so on.

In your own code, you could do something like this (just to give you an idea):

let rec Block (formatters : Formatter list) (itw : IndentedTextWriter) =
    itw.WriteLine "{"
    IndentedTextWriter.indented itw <| fun itw ->
        formatters |> List.iter (fun fmtr -> fmtr itw)
    itw.WriteLine "}"

One other note about your pseudo-code: because your formatting state is mutable (as is IndentedTextWriter in my code) there's really no need to pass it out of your functions -- that is, you generally only need to create functions which take and return a state value when those states are represented by an immutable object/value.

Oddly, when passing around a mutable writer (as in our code here) you actually want the "reader" workflow or some variant of it. ExtCore contains "reader"-style functions for List, Array, etc., in the ExtCore.Control.Collections.Reader module which you could use to further simplify your code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM