Wednesday, November 18, 2009

DynamicObject over M values

If you've used our bits, then for sure you have played with the DSL features.

If you haven't, it lets you create a DSL to process text and produce structured data.

With dynamic object support in .Net 4.0, we can add the ability to interact with that data as if it were objects in the CLR.

The code below is just such a little library that lets you do so. Don wrote it last night to help out with our PDC talk today.

Try it out!

// implementation of .NET 4.0 dynamic object over Node/Edge data model

// provides a read-only object-based view over M values

namespace Microsoft.M

{

using System.Reflection;

using System.Dataflow;

using System.IO;

using System.Dynamic;

using System;

using System.Collections;

using System.Collections.Generic;

public class DynamicObjectNode : DynamicObject

{

Node node;

// we never want to double-wrap, so all construction

// goes through NodeToObject, which unwraps atomics

internal DynamicObjectNode(Node node)

{

this.node = node;

}

// this method is the only way to go from the low-level

// node world to the world of (dynamic) objects

public static object NodeToObject(Node node)

{

if (node.NodeKind == NodeKind.Atomic)

return node.AtomicValue;

else

return new DynamicObjectNode(node);

}

public static IEnumerable<dynamic> ReadValues(TextReader reader)

{

foreach (var edge in Node.ReadFrom(reader))

{

yield return NodeToObject(edge.Node);

}

}

public static IEnumerable<dynamic> ReadValuesFromString(string text)

{

foreach (var edge in Node.ReadFromString(text))

{

yield return NodeToObject(edge.Node);

}

}

// implement the obvious three...

public override bool Equals(object obj)

{

return node.Equals(obj);

}

public override int GetHashCode()

{

return node.GetHashCode();

}

public override string ToString()

{

return node.ToString();

}

public Identifier GetBrand() { return node.Brand; }

// special case conversion to IEnumerable to support foreach

// over lists and collections

public override bool TryConvert(ConvertBinder binder, out object result)

{

result = null;

if (binder.Type != typeof(IEnumerable))

return false;

switch (node.NodeKind)

{

case NodeKind.Collection:

result = WrapCollectionNodeAsEnumerable(node);

return true;

case NodeKind.List:

result = WrapListNodeAsEnumerable(node);

return true;

default:

return false;

}

}

IEnumerable WrapCollectionNodeAsEnumerable(Node node)

{

foreach (var item in node.Edges)

{

yield return NodeToObject(item.Node);

}

}

IEnumerable WrapListNodeAsEnumerable(Node node)

{

foreach (var item in node.ViewAsList())

{

yield return NodeToObject(item);

}

}

// support [int] over list and [string] over records

public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)

{

result = null;

if (indexes.Length > 1)

return false;

if (node.NodeKind == NodeKind.List && indexes[0] is int)

{

int index = (int)indexes[0];

var list = node.ViewAsList();

if (index >= list.Count)

throw new IndexOutOfRangeException("Indexed past end of M list node");

result = NodeToObject(list[index]);

return true;

}

string name = indexes[0] as string;

if (name != null)

{

return TryGetMember(name, true, out result);

}

return false;

}

// support . over records and .Count/.IsReadOnly over collections/lists

public override bool TryGetMember(GetMemberBinder binder, out object result)

{

return TryGetMember(binder.Name, binder.IgnoreCase, out result);

}

bool TryGetMember(string name, bool ignoreCase, out object result)

{

result = null;

switch (node.NodeKind)

{

case NodeKind.List:

if (name == "Count")

{

result = node.ViewAsList().Count;

return true;

}

else if (name == "IsReadOnly")

{

result = true;

return true;

}

return false;

case NodeKind.Collection:

if (name == "Count")

{

result = node.Edges.Count;

return true;

}

else if (name == "IsReadOnly")

{

result = true;

return true;

}

return false;

case NodeKind.Record:

if (node.Edges.ContainsLabel(name))

{

result = NodeToObject(node.Edges[name]);

return true;

}

else if (ignoreCase)

{

foreach (var e in node.Edges)

{

if (string.Compare(e.Label.Text, name, StringComparison.InvariantCultureIgnoreCase) == 0)

{

result = NodeToObject(e.Node);

return true;

}

}

}

return false;

default:

return false;

}

}

}

public class Language

{

MImage image;

GraphStore store = new DefaultGraphStore();

string moduleName, languageName;

ParserFactory factory;

internal Language(MImage image, string moduleName, string languageName)

{

this.image = image;

this.moduleName = moduleName;

this.languageName = languageName;

this.factory = this.image.ParserFactories[moduleName + "." + languageName];

}

internal Language(Assembly assm, string moduleName, string languageName)

: this(MImage.LoadFromAssembly(assm), moduleName, languageName)

{

}

public MImage Image { get { return this.image; } }

public string ModuleName { get { return moduleName; } }

public string LanguageName { get { return languageName; } }

public static Language Load(MImage image, string moduleName, string languageName)

{

return new Language(image, moduleName, languageName);

}

public static Language Load(Assembly assm, string moduleName, string languageName)

{

return new Language(assm, moduleName, languageName);

}

public static Language LoadFromCurrentAssembly(string moduleName, string languageName)

{

var assm = Assembly.GetCallingAssembly();

return new Language(assm, moduleName, languageName);

}

public dynamic ParseString(string text)

{

return Parse(new StringReader(text));

}

public dynamic ParseString(string text, ErrorReporter errors)

{

return Parse(new StringReader(text), errors);

}

public dynamic ParseString(string text, ErrorReporter errors, string fileName)

{

return Parse(new StringReader(text), errors, fileName);

}

public dynamic ParseString(string text, ErrorReporter errors, string fileName, SourcePoint startLocation)

{

return Parse(new StringReader(text), errors, fileName, startLocation);

}

public dynamic Parse(TextReader reader)

{

return Parse(new TextReaderTextStream(reader), ErrorReporter.Standard);

}

public dynamic Parse(TextReader reader, ErrorReporter errors)

{

return Parse(new TextReaderTextStream(reader), errors);

}

public dynamic Parse(TextReader reader, ErrorReporter errors, string fileName, SourcePoint startLocation)

{

return Parse(new TextReaderTextStream(fileName, reader), errors, startLocation);

}

public dynamic Parse(TextReader reader, ErrorReporter errors, string fileName)

{

return Parse(new TextReaderTextStream(fileName, reader), errors);

}

public dynamic Parse(ITextStream stream, ErrorReporter errors)

{

var parser = CreateParser();

var result = parser.Parse(stream, errors);

return NormalizeResult(result);

}

public dynamic Parse(ITextStream stream, ErrorReporter errors, SourcePoint startLocation)

{

var parser = CreateParser();

var result = parser.Parse(stream, errors, startLocation);

return NormalizeResult(result);

}

System.Dataflow.Parser CreateParser()

{

var parser = factory.Create();

parser.GraphBuilder = new NodeGraphBuilder(store);

return parser;

}

dynamic NormalizeResult(object result)

{

if (result is Node)

return DynamicObjectNode.NodeToObject((Node)result);

return result;

}

}

}

4 comments:

Guirec said...

Hi Jeff,
I enjoyed your demo with Don at PDC and am interested in implementing my own MGrammar.

Could you send me a zip of the solution you used at pdc?
Many thanks,
Guirec.

Unknown said...

Significant limitations to this approach:

(1) Can't define extension methods for dynamic types.

(2) A cast is required to utilize a BCL method, unless TryConvert() already knows about the type.

(3) The lack of IntelliSense with dynamic types is somewhat maddening. I'm definitely spoiled by the IntelliSense provided with strongly-typed variables.

Although this example code is koolio, programming against the DefaultGraphStore is not that difficult, and retains the advantage of strong typing.

YSharp said...

Please, keep up with your great work, the team!

On my side, sadly too busy with the stuff I do for a living, I couldn't progress much in my experimens (grr..) but I'm getting back to it (ha!) now, and I hopefully should have more of a "Tea Party" soon, to have (y)our dear "Oslo" "rock" even more! ;)

http://lambda-the-ultimate.org/node/3739

http://www.ysharp.net/the.language/rationale/T_Party_0.html

Take the care!

Y#

YSharp said...

(Just sharing)

http://lambda-the-ultimate.org/node/3743#comment-53447