Sunday, September 5, 2010

HyperJS Episode 4 - A New Hope?

So Close, Yet So Far Away

Posts in the HyperJS Series

Last time, I discussed the third step on the road to JavaScript in C# in my HyperJS Episode 3 - Revenge of the Script post. It covers the combination of the HyperCore classes HyperDictionary and HyperDynamo into HyperHypo to enable JavaScript-style getter and setter notation in C#. It continued to cover prototype-based inheritance along with the magic of Closures in C# to allow creation of dynamic objects as inline functions. All of the projects I talk about in this series are available on my GitHub repos: http://github.com/tchype.

In part 4, the adventure continues and the naming gets slightly better. Now that I have what seems to be reasonable JavaScript-approximated syntax inside of C#, how far can I take it? What better way to find out than trying to implement some core features of JavaScript using HyperHypo as the basis, called HyperJS.

HyperJS Part 4: HyperJS Starts Taking Form

In starting HyperJS, the goal was to try to quickly prototype it and see if there are any show-stoppers, if I just get sick of the impedance mismatch, or to see if all signs point to awesomeness.

Prototype Goals

I set out with some basic goals, focused around core pieces of JavaScript that I think could be trouble:

  • Try to implement the Global Object: All functions that are "global" hang off of this object (as do any variables not declared with var)
  • Including other objects/scripts: Can I enable the inclusion of other scripts/objects simply with "using" statements rather than having some kind of class loader?
  • Implement Object, String and Boolean basics, including Feature Detection: Just get some basics going, and wouldn't it be great to ask C# if a class implemented a function with if (foo.bar)?
  • Functions and prototypes: Can I overcome the limitation that functions are not dynamic-able objects in C#?

undefined, Not RuntimeBinderException

The first step was to create the base JavaScript Object class that everything else would be based on. The main difference here was to ensure that when a member is not found, we want to get an "undefined" object back instead of the RuntimeBinderException that C# dynamic objects throw. Thus, JSObject was born and inherits (in a C# sense) from HyperHypo and changes that behavior. It is the basis for all things in HyperJS.

The Global Object is JS.cs or JS.go

For various reasons I'll cover shortly, it became quickly obvious that I needed a Global Object. Mainly, this was where the Boolean(value), String(value), and Object(value) functions resided that I was planning on starting to implement, so they needed a home. So, I created a JS class that inherits from JSObject and is a singleton. Since the object is global, there should only every be one instance of that object.

I created two "instance" members on JS, one called cs and one called go. I called the first "cs" for "C#" since it is a strongly-typed C# accessor for the global object instance typed to JS; this is important, and I'll cover it briefly. I called the second "go" for "global object" and it returns the same object but is typed as "dynamic" so you can access any dynamically added members that one may decide to add to the global object later without the compiler bitching at you.

Adding Items to the Global Object With using

I decided to try and avoid going the route of a language on top of the DLR or an "Engine", so how to "load" objects into scope for use? I thought following the style of the Linq constructs only showing up with a using statement was elegant, so I attempted that. Two key things came out of trying to enable this:

  1. Having a strongly-typed instance variable for the global object allows for extension methods to be "added" to the global object JS.cs through C#'s syntactic sugar.
  2. Without a real C# class, literally attaching objects or functions to the global object will have to be added on first-use (lazy loading) and added through the dynamic instance member JS.go.

Core JavaScript objects would live in the same namespace as the JS class, so they would appear to be part of the JS.cs object at all times (much like most of the functionality of ASP.NET MVC's HtmlHelper is actually implemented through extension methods to keep the core of HtmlHelper clean). Things like DOM objects would be implemented in a separate namespace to make sure the core HyperJS stays as pure as possible.

String and Boolean

Below are examples where I have implemented String and Boolean as extension methods Boolean(value) for the function call and NewBoolean(value) for the constructor method, and similar ones for String. (Ignore the details around Prototype and JSTypeName for now; I will cover them shortly)

JSBoolean.js:
using System;

namespace TonyHeupel.HyperJS
{
    public static class JSBoolean
    {
        /// 
        /// The Boolean(value) function on the global object.
        /// It returns a Boolean converted from the value passed in. 
        /// 0, NaN, null, "", undefined, false and "false" are false.
        /// Everything else will return true.
        /// 
        public static bool Boolean(this JS js, object value)
        {
            return NewBoolean(js, value).valueOf();
        }

        /// 
        /// The Boolean(value) constructor function.
        /// It returns a Boolean object converted from the value passed in.
        /// 
        public static dynamic NewBoolean(this JS js, dynamic value)
        {
            return BooleanConstructor(js, value);
        }

        private static dynamic BooleanConstructor(this JS js, dynamic value)
        {
            dynamic b = new JSObject();
            b.JSTypeName = "Boolean";

            // Set up prototype
            dynamic p = new JSObject();
            b.Prototype = b.GetPrototype(p);

            // Calculate the primitive value
            bool _primitiveValue = true; //default to true and only set to false when needed
            dynamic v = (value is JSObject && !(value is JSUndefined || value is JSNaN)) ? value.valueOf() : value;
            if (v == null ||
                (v is String && (v == "" || v == "false")) ||
                (v is Boolean && v == false) ||
                ((v is Int32 || v is Int64 || v is Int16) && v == 0) ||
                v is JSNaN ||
                v is JSUndefined)
            {
                _primitiveValue = false;
            }


            // Set up instance items
            b.valueOf = new Func<bool>(() => _primitiveValue);
            b.toString = new Func<string>(() => _primitiveValue ? "true" : "false"); //Consider using String() here

            return b;
        }
    }
}

JSString.js
using System;

namespace TonyHeupel.HyperJS
{
    public static class JSString
    {
        public static string String(this JS js, dynamic value)
        {
            return NewString(js, value).valueOf();
        }

        public static dynamic NewString(this JS js, dynamic value)
        {
            return StringConstructor(js, value);
        }

        private static dynamic StringConstructor(this JS js, dynamic value)
        {
            dynamic s = new JSObject();
            s.JSTypeName = "String";

            // Set up the prototype first
            dynamic p = new JSObject();
            s.Prototype = s.GetPrototype(p);

            // Set up the instance behavior
            var _primitiveValue = (value == null) ? null : (value is JSObject && JS.cs.Boolean(value.toString as string)) ? value.toString() : value.ToString();

            s.toString = s.valueOf = new Func<string>(() => _primitiveValue);

            return s;
        }
    }
}

And to use these things looks like this (in unit test format):

[TestMethod]
public void BooleanFunctionReturnsFalseProperly()
{
  Assert.IsFalse(JS.cs.Boolean(null));
  Assert.IsFalse(JS.cs.Boolean(0));
  Assert.IsFalse(JS.cs.Boolean(false));
  Assert.IsFalse(JS.cs.Boolean(string.Empty));
  Assert.IsFalse(JS.cs.Boolean("false"));
  Assert.IsFalse(JS.undefined);
  Assert.IsFalse(JS.NaN);

  Assert.IsFalse(JS.cs.NewBoolean(0));  // Implicit cast from JSObject to bool - nice!
            
}

[TestMethod]
public void BooleanFunctionReturnsTrueProperly()
{
  Assert.IsTrue(JS.cs.Boolean("False"));
  Assert.IsTrue(JS.cs.Boolean(new object()));
            
  dynamic someThing = JS.cs.NewObject();
  Assert.IsTrue(someThing);
  someThing.foobar = 5;
  Assert.IsTrue(JS.cs.Boolean(someThing.foobar as object));
  Assert.IsTrue(JS.cs.Boolean(" "));

  Assert.IsTrue(JS.cs.NewBoolean(" ")); // Implicit cast from JSObject to bool - nice!
}

[TestMethod]
public void StringConstructorFunctionReturnsStringProperly()
{
  dynamic s = JS.cs.NewString("hello");
  Assert.IsInstanceOfType(s, typeof(JSObject));
  Assert.AreEqual("String", s.JSTypeName);
            
  Assert.IsInstanceOfType(s.valueOf(), typeof(String));
  Assert.IsInstanceOfType(s.toString(), typeof(String));
  Assert.AreEqual("hello", s.valueOf());
  Assert.AreEqual("hello", s.toString());
}

As you can see, instead of calling

var s = new String("hello");
you use
dynamic s = JS.cs.NewString("hello");
That seems like a pretty straightforward translation so far!

And What About Non-Core Objects?

Over in my HyperActive project, I wanted to define an Image class and attach it into the global namespace without having to modify the HyperJS core library. It was very easy

HyperActive\HyperActive.ConsoleApp\Image.cs:
using TonyHeupel.HyperJS;

namespace TonyHeupel.HyperActive.JSExtensions
{
    public static class Image
    {
        public static dynamic NewImage(this JS js)
        {
            return NewImage(js, 0, 0);
        }

        public static dynamic NewImage(this JS js, int width, int height)
        {
            dynamic img = new JSObject();
            img.JSTypeName = "Image";

            // Set up the prototype
            dynamic p = new JSObject();
            img.Prototype = img.GetPrototype(p);  // Should actually create a DOM Element base class with name and id and use that...
            
            // Set up instance items
            img.width = width;
            img.height = height;

            img.id = "";
            img.name = "";
            img.src = "";
            img.alt = "";           // Alternate text when image can't be displayed
            img.isMap = false;      // Whether to use a server-side image map
            img.longDesc = "";      // Uri of a long image description
            img.useMap = "";        // Specifies a client-side image map for the image

            return img;
        }
    }
}
And to use it in the console app's Main function:
using TonyHeupel.HyperActive.JSExtensions; // To get the Image class

namespace HyperActive.ConsoleApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Second example: I created a HyperJS (JavaScript using HyperDynamo) Image class\nusing extension methods so that it looks like it's baked into HyperJS but isn't\n(see code Image.cs in the ConsoleApp project for details)\n==============================");
      dynamic img = JS.cs.NewImage(20, 30);
      dynamic img2 = JS.cs.NewImage();

      Console.WriteLine("\nimg.width: {0}\nimg.height: {1}", img.width, img.height);
      Console.WriteLine("\nimg.toString(): {0}\n", img.toString());
      Console.WriteLine("\mimg2.toString(): {0}\n", img2.toString());
    }
  }
}

This Looks Promising

Well, that seems pretty straightforward! In a very basic way, I have proven that I can cover the first three goals of the prototype without much risk. Certainly there are plenty of other things to consider (should I use real C# classes instead of functions, etc.), but things seem to be going well overall for some very basic cases. Next up: overcoming prototypes and functions not being first-class objects in C#.

It turns out that this is actually a serious issue and may be a show-stopper for HyperJS (or at least my personal effort into making HyperJS a reality). Stay tuned for the exciting installment: HyperJS Episode 5 - The Prototype Strikes Back!

No comments: