Saturday, November 24, 2007

Immutable data class generator: it lives!

Well, it works. You invoke like this:

. .\Library.DataClass.ps1

class MyClass {

    field ([string]) S

    field ([int]) I

} > MyClass.cs

And then you can write:

MyClass mc = new MyClass.Builder().SetS("xxx").ToMyClass();

MyClass mc2 = new MyClass.Builder(mc).SetS("yyy").ToMyClass();

And the output looks like:

internal partial class MyClass

{

public readonly System.String S;

public readonly System.Int32 I;


 

public MyClass(string S, int I)

{

this.S = S;

this.I = I;

}

public class Builder

{

public string S;

public int I;

public Builder()

{

}

public Builder(MyClass value)

{

this.S = value.S;

this.I = value.I;

}

public virtual MyClass ToMyClass()

{

return new MyClass(this.S, this.I);

}

public virtual Builder SetS(string value)

{

this.S = value;

return this;

}

public virtual Builder SetI(int value)

{

this.I = value;

return this;

}

}

}

The main issue is that the ToMyClass and Set* methods are virtual. I think I may be hitting a bug in PowerShell, but I'm still researching it. For now, this will have to do.

Here's the implementation of Library.DataClass.ps1:

function Class

{

    param (

        [String] $name,

        [ScriptBlock] $memberScriptBlock

    )


 

    $class = New-Object System.CodeDom.CodeTypeDeclaration $name

    $class.TypeAttributes = [System.Reflection.TypeAttributes]::NotPublic

    $class.IsPartial = $true


 

    $constructor = New-Object System.CodeDom.CodeConstructor

    $constructor.Attributes = [System.CodeDom.MemberAttributes]::Public

    $class.Members.Add( $constructor ) | Out-Null

    

    $builderClass = New-Object System.CodeDom.CodeTypeDeclaration "Builder"

    $class.Members.Add( $builderClass ) | Out-Null


 

    $builderConstructor = New-Object System.CodeDom.CodeConstructor

    $builderConstructor.Attributes = [System.CodeDom.MemberAttributes]::Public

    $builderClass.Members.Add( $builderConstructor ) | Out-Null

    

    $builderConstructor2 = New-Object System.CodeDom.CodeConstructor

    $builderConstructor2.Attributes = [System.CodeDom.MemberAttributes]::Public

    $builderConstructor2.Parameters.Add(

        (New-Object System.CodeDom.CodeParameterDeclarationExpression( $name, "value" ))

    ) | Out-Null

    $builderClass.Members.Add( $builderConstructor2 ) | Out-Null

    

    $realizeMethod = New-Object System.CodeDom.CodeMemberMethod

    $realizeMethod.Attributes = [System.CodeDom.MemberAttributes]::Public

    $realizeMethod.Name = "To$name"

    $realizeMethod.ReturnType = $name


 

    $ctorExpression = New-Object System.CodeDom.CodeObjectCreateExpression

    $ctorExpression.CreateType = New-Object System.CodeDom.CodeTypeReference($name)

    $realizeMethod.Statements.Add(

        (New-Object System.CodeDom.CodeMethodReturnStatement(

            $ctorExpression

        ))

    ) | Out-Null

        

    $builderClass.Members.Add( $realizeMethod ) | Out-Null


 

    # return a hash of the CodeDom objects related to

    # this field

    function field

    {

        param (

            [Type] $type,

            [String] $name

        )

        

        @{

            type = $type

            name = $name

            readonlyFieldDeclaration = $(

# CodeDom doesn't support 'readonly' fields.

# See http://blogs.msdn.com/bclteam/archive/2005/03/16/396915.aspx

#                $field = New-Object System.CodeDom.CodeMemberField($type, $name)

#                $field.Attributes = [System.CodeDom.MemberAttributes]::Public

#                $field

                New-Object System.CodeDom.CodeSnippetTypeMember("`tpublic readonly $type $name;`n")

                )

            fieldDeclaration = $(

                $field = New-Object System.CodeDom.CodeMemberField($type, $name)

                $field.Attributes = [System.CodeDom.MemberAttributes]::Public

                $field

            )

            parameter = New-Object System.CodeDom.CodeParameterDeclarationExpression($type, $name)

            fieldReference = New-Object System.CodeDom.CodeFieldReferenceExpression(

                (New-Object System.CodeDom.CodeThisReferenceExpression),

                $name

            )

            parameterReference = New-Object System.CodeDom.CodeVariableReferenceExpression $name

            setMethodName = "Set$name"

        }                

    }

    

    & $memberScriptBlock | foreach {

        $class.Members.Add( $_.readonlyFieldDeclaration )

        

        $constructor.Parameters.Add( $_.parameter )


 

        $constructor.Statements.Add(

            $(New-Object System.CodeDom.CodeAssignStatement( $_.fieldReference, $_.parameterReference ))

        )

        

        $builderConstructor2.Statements.Add(

            (New-Object System.CodeDom.CodeAssignStatement(

                $_.fieldReference,

                (New-Object System.CodeDom.CodeFieldReferenceExpression(

                    (New-Object System.CodeDom.CodeVariableReferenceExpression "value"),

                    $_.name

                ))

            ))

        )

        

        $builderClass.Members.Add( $_.fieldDeclaration ) | Out-Null

        $setMethod = New-Object System.CodeDom.CodeMemberMethod

        $setMethod.Name = $_.setMethodName


 

        $setMethod.ReturnType = $builderClass.Name

        # This should be Public,Final, but that fails for me. Possible PowerShell bug?

        $setMethod.Attributes = [System.CodeDom.MemberAttributes] "Public"

        $setMethod.Statements.Add(

            (New-Object System.CodeDom.CodeAssignStatement(

                $_.fieldReference,

                (New-Object System.CodeDom.CodeVariableReferenceExpression ("value"))

            ))

        )

        $setMethod.Statements.Add(

            (New-Object System.CodeDom.CodeMethodReturnStatement(

                (New-Object System.CodeDom.CodeThisReferenceExpression)

            ))

        )

        

        $setMethod.Parameters.Add(

            (New-Object System.CodeDom.CodeParameterDeclarationExpression( $_.type, "value" ))

        )

        $builderClass.Members.Add( $setMethod )

        

        $ctorExpression.Parameters.Add( $_.fieldReference )

        

    } | Out-Null    

    

    $csharpCodeProvider = New-Object Microsoft.CSharp.CSharpCodeProvider

    $sw = New-Object System.IO.StringWriter

    $codeGeneratorOptions = New-Object System.CodeDom.Compiler.CodeGeneratorOptions

    $codeGeneratorOptions.BracingStyle = "C"

    $codeGeneratorOptions.BlankLinesBetweenMembers = $false

    

    $csharpCodeProvider.GenerateCodeFromType( $class, $sw, $codeGeneratorOptions )

    

    $sw.ToString()

}

PowerShell DSLs: Using hashtables and scriptblocks together

Previously I presented the use of hashtables and scriptblocks as DSL input formats. You can also convert between them, with processing in between. I tried that out, and I think the result is interesting enough to post. Again, here is the input:

class MyClass {

    field ([string]) S

}

The previous code would use the 'field' function to manipulate the class object as appropriate:

    function field

    {

        param (

            [Type] $type,

            [String] $name

        )

        

        # add the field declaration

        $field = New-Object System.CodeDom.CodeMemberField($type, $name)

        $field.Attributes = [System.CodeDom.MemberAttributes]::Public

        $class.Members.Add( $field )

        

        # add the ctor parameter

        $ctorParameter = New-Object System.CodeDom.CodeParameterDeclarationExpression($type, $name)

        $constructor.Parameters.Add($ctorParameter)


 

        # add the ctor initializer

        $fieldReference = New-Object System.CodeDom.CodeFieldReferenceExpression(

            (New-Object System.CodeDom.CodeThisReferenceExpression),

            $name

        )

        

        $ctorInitializer = New-Object System.CodeDom.CodeAssignStatement(

            $fieldReference,

            (New-Object System.CodeDom.CodeVariableReferenceExpression $name)

        )

        

        $constructor.Statements.Add( $ctorInitializer )

    }

    

    & $memberScriptBlock | Out-Null


 

But another idea is to have the 'field' function produce a collection of CodeDom objects that can be assembled later.

    # return a hash of the CodeDom objects related to

    # this field

    function field

    {

        param (

            [Type] $type,

            [String] $name

        )

        

        @{

            fieldDeclaration = $(

                $field = New-Object System.CodeDom.CodeMemberField($type, $name)

                $field.Attributes = [System.CodeDom.MemberAttributes]::Public

                $field

                )

            parameter = New-Object System.CodeDom.CodeParameterDeclarationExpression($type, $name)

            fieldReference = New-Object System.CodeDom.CodeFieldReferenceExpression(

                (New-Object System.CodeDom.CodeThisReferenceExpression),

                $name

            )

            parameterReference = New-Object System.CodeDom.CodeVariableReferenceExpression $name

        }                

    }

    

    & $memberScriptBlock | foreach {

        $class.Members.Add( $_.fieldDeclaration )

        

        $constructor.Parameters.Add( $_.parameter )


 

        $constructor.Statements.Add(

            $(New-Object System.CodeDom.CodeAssignStatement( $_.fieldReference, $_.parameterReference ))

        )

    } | Out-Null

I suspect that the latter model is a little better because it separates concerns. Consider if I were to add other statements to my language, such as 'property'. The 'foreach' at the end could probably be written in a way that works for both fields and properties. However, the hashtable is slightly concerning, because it's not typed – if I get the key names wrong somewhere, I'm screwed.

I find that I'm spending quite a lot of time on this, but it's important that I find a way to create DSLs quickly. I figured that right now I'm just learning the techniques, and then if I master them, then I can do it more quickly when the time comes.

Friday, November 23, 2007

Immutable data class generator: Skeleton implementation

I've made a little progress on my PowerShell DSL for data classes, and figured it's a good time to show it off.

Here's the input format at this point:

class MyClass {

field ([string]) S

}

(I'm using the ScriptBlock approach.) I generally like the syntax, except for the need to add parentheses around the type. The alternative is to use a fully-qualified string:

class MyClass {

field System.String S

}

I'm not sure which is better, but I'm going with the first one for now, because it allows a more specific type than string.

The basis of the implementation is to write methods named 'class' and 'field'. 'class' is simple; here's an excerpt of the important bits:

$class = New-Object System.CodeDom.CodeTypeDeclaration $name

$class.TypeAttributes = [System.Reflection.TypeAttributes]::NotPublic

$class.IsPartial = $true

$constructor = New-Object System.CodeDom.CodeConstructor

$class.Members.Add( $constructor ) Out-Null

$class

The implementation of 'field' could go in several different ways. For now, I've written it to manipulate the class as appropriate, e.g.:

$field = New-Object System.CodeDom.CodeMemberField($type, $name)

$class.Members.Add( $field )


$ctorParameter = New-Object System.CodeDom.CodeParameterDeclarationExpression($type, $name)

$constructor.Parameters.Add($ctorParameter)


$fieldReference = New-Object System.CodeDom.CodeFieldReferenceExpression(

(New-Object System.CodeDom.CodeThisReferenceExpression),

$name

)


$ctorInitializer = New-Object System.CodeDom.CodeAssignStatement(

$fieldReference,

(New-Object System.CodeDom.CodeVariableReferenceExpression $name)

)


$constructor.Statements.Add( $ctorInitializer )


(I'd like to attach the full script, but I don't see a way to do that in blogspot). It generates this output:

internal partial class MyClass

{

public string S;

private MyClass(string S)

{

this.S = S;

}

}



EDIT: See PowerShell DSLs: Using hashtables and scriptblocks together for a different view of this code.

Immutable data class generator

I'm working on a DSL implementation for defining "data classes" with certain properties in C# (see http://blogs.gotdotnet.com/ericlippert/archive/2007/11/13/immutability-in-c-part-one-kinds-of-immutability.aspx for an example of the output).

Table of Contents

  1. Goals
  2. Skeleton implementation
  3. ???

PowerShell DSLs: Script block input

As an alternative to using a hashtable for input, you can use a script block. Here's a script-block-way of doing the same thing as before:

class MyClass -accessibilty:public {

member string S

}


and here's one of many ways of handling it

function class {

param (

[String] $name = $( throw
"name is required" ),

[String] $accessibilty,

[ScriptBlock] $memberScriptBlock = $( throw
"member script block is require" )

)


" $accessibilty class $name {"


& $memberScriptBlock % { " public $($_.declaration)" }


" }"

}


function member {

param (

[String] $type = $( throw
"type is required" ),

[String] $name = $( throw
"name is required" )

)


@{

declaration = "$type $name;"


}

}


What's happening here is that the first word on any line becomes a function, and the remaining words are parameters to that function. So, you're now creating an "internal DSL" in PowerShell. This lets you work in a more idoimatic PowerShell manner.


PowerShell DSLs: Hash table input

lassOne way to take PowerShell DSL input is in the form of a hash table. Taking from the Data Type with Builder example, you could write input as:

@{
kind = 'class'
name = 'MyClass'
accessibilty = 'public'
members = @{
type = 'string'
name = 'S'
}
}


How do you consume this input? Here's an example. (Note that the code generation side is not the focal point here -- the way that we interpet the input is what matters.)


if ($input.kind = 'class') {
" $($input.accessibilty) class $($input.name) {"

$input.members | foreach {
" public $($_.type) $($_.name);"
}

" }"

}


Which generates this output:


public class MyClass {
public System.String S;
}


This approach seems most effective when you need to express a lot of attributes on a single element. The downside is that the input seems a bit verbose and unnatural in some cases.

PowerShell DSLs

I've written before about my attraction to Domain-Specific Languages and my curiosity about using PowerShell to handle them, but I only recently got any time to think about it more. I'm working on a DSL implementation for defining "data classes" with certain properties in C# (see http://blogs.gotdotnet.com/ericlippert/archive/2007/11/13/immutability-in-c-part-one-kinds-of-immutability.aspx#comments for an example).

I have only 3 examples of PowerShell DSLs to work from, and I don't see any guidance in this area, so I figured I'd write about what I find.

Table of Contents

  1. Prior art
  2. Why PowerShell?
  3. Custom invocation w/ a library
  4. DSL tool invocation
  5. Hash table input
  6. Script block input
  7. Using hashtables and scriptblocks together

Warning: technical content ahead

While working at Microsoft, I had a blog for work-related issues at blogs.msdn.com. Now I need a new place to write these things, so this blog will now include technical content, along with the food, politics, family, etc. that have come before.

Friday, November 09, 2007

Reid's Journal Entry: Making Apple Crisp

Today Reid made apple crisp. I wanted to know more about it, so I asked him some questions.

J: How many oranges did you use?
R: Zero oranges. It only takes apples and a few other ingredients, which are kind of like spices. Butter,

J: Oranges?
R: No... I mean, yes

J: What else?
R: Weeds. Then we milk and cereal. This is going to be so funny! Don't write that.

J: After you mixed it up, what did you do?
R: I baked it for 2000 hours. What zee doh.

J: How did it taste?
R: It's not ready for that part yet. We added 2000 drops of stevia in. Time for what it tasted like. It tasted like... stevia. Of course it would.
 
Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.