Fluent-Style Programming in JavaScript
I’ve been playing around with JavaScript a great deal lately and trying to find my way. I last programmed JS seriously about 10 years ago and it’s amazing to me how much the world has changed since then. For example, the fifth edition of ECMAScript (ES5) has recently been approved for standardization and it’s already widely implemented in modern browsers, including my favorite browser, IE9.
Fluent LINQ
However, I’m a big C# fan, especially the fluent API style of LINQ methods like Where, Select, OrderBy, etc. As an example, assume the following C# class:
class Person { public Person() { Children = new List<Person>(); } public string Name { get; set; } public DateTime Birthday { get; set; } public int Age { get { return (int)((DateTime.Now - Birthday).Days / 365.25); } } public ICollection<Person> Children { get; private set; } public override string ToString() { return string.Format("{0} ({1})", Name, Age); } }
var chris = new Person() { Name = "Chris", Birthday = new DateTime(1969, 6, 2), Children = { new Person() { Name = "John", Birthday = new DateTime(1994, 5, 5), }, new Person() { Name = "Tom", Birthday = new DateTime(1995, 8, 30), }, }, };
var people = new Person[] { chris }.Union(chris.Children); Console.WriteLine("People: " + people.Aggregate("", (s, p) => s + (s.Length == 0 ? "" : ", ") + p.ToString())); Console.WriteLine("Teens: " + people.Where(p => p.Age > 12 && p.Age < 20). Aggregate("", (s, p) => s + (s.Length == 0 ? "" : ", ") + p.ToString()));
People: Chris (41), John (16), Tom (15) Teens: John (16), Tom (15)
Fluent JavaScript
// Person constructor function Person(args) { if (args.name) { this.name = args.name; } if (args.birthday) { this.birthday = args.birthday; } if (args.children) { this.children = args.children; } } // Person properties and methods Person.prototype = Object.create(null, { name: { value: "", writable: true }, birthday: { value: new Date(), writable: true }, age: { get: function () { return Math.floor((new Date() - this.birthday) / 31557600000); } }, children: { value: [], writable: true }, toString: { value: function () { return this.name + " (" + this.age + ")"; } } });
I can do several LINQ-style things on it:
var s = ""; var tom = new Person({ name: "tom", birthday: new Date(1995, 7, 30) }); var john = new Person({ name: "john", birthday: new Date(1994, 4, 5) }); var chris = new Person({ name: "chris", birthday: new Date(1969, 5, 2), children: [tom, john] }); var people = [tom, john, chris]; // select s += "<h1>people</h1>" + people.map(function (p) { return p; }).join(", "); // where s += "<h1>teenagers</h1>" + people.filter(function (p) { return p.age > 12 && p.age < 20 }).join(", "); // any s += "<h1>any person over the hill?</h1>" + people.some(function (p) { return p.age > 40; }); // aggregate s += "<h1>totalAge</h1>" + people.reduce(function (totalAge, p) { return totalAge += p.age; }, 0); // take s += "<h1>take 2</h1>" + people.slice(0, 2).join(", "); // skip s += "<h1>skip 2</h1>" + people.slice(2).join(", "); // sort s += "<h1>sorted by name</h1>" + people.slice(0).sort( function (lhs, rhs) { return lhs.name.localeCompare(rhs.name); }).join(", "); // dump document.getElementById("output").innerHTML = s;
Notice that several things are similar between JS and C# LINQ-style:
- The array and object initialization syntax looks very similar so long as I follow the JS convention of passing in an anonymous object as a set of constructor parameters.
- The JS Date type is like the .NET DateTime type except that months are zero-based instead of one-based (weird).
- When a Person object is “added” to a string, JS is smart enough to automatically call the toString method.
- The JS map function lets you project from one set to another like LINQ Select.
- The JS filter function lets you filter a set like LINQ Where.
- The JS some function lets you check if anything in a set matches a predicate like LINQ Any.
- The JS reduce function lets you accumulate results from a set like the LINQ Aggregate.
- The JS slice function is a multi-purpose array manipulation function that we’ve used here like LINQ Take and Skip.
- The JS slice function also produces a copy of the array, which is handy when handing off to the JS sort, which acts on the array in-place.
The output looks as you’d expect:
We’re not all there, however. For example, the semantics of the LINQ First method are to stop looking once a match is found. Those semantics are not available in the JS filter method, which checks every element, or the JS some method, which stops once the first matching element is found, but returns a Boolean, not the matching element. Likewise, the semantics for Union and Single are also not available as well as several others that I haven’t tracked down. In fact, there are several JS toolkits available on the internet to provide the entire set of LINQ methods for JS programmers, but I don’t want to duplicate my C# environment, just the set-like thinking that I consider language-agnostic.
So, in the spirit of JS, I added methods to the build in types, like the Array type where all of the set-based intrinsics are available, to add the missing functionality:
Object.defineProperty(Array.prototype, "union", { value: function (rhs) { var rg = this.slice(0); rhs.forEach(function (v) { rg.unshift(v); }) return rg; }}); Object.defineProperty(Array.prototype, "first", { value: function (callback) { for (var i = 0, length = this.length; i < length; ++i) { var value = this[i]; if (callback(value)) { return value; } } return null; }}); Object.defineProperty(Array.prototype, "single", { value: function (callback) { var result = null; this.forEach(function (v) { if (callback(v)) { if (result != null) { throw "more than one result"; } result = v; } }); return result; }});
These aren’t perfectly inline with all of the semantics of the built-in methods, but they give you a flavor of how you can extend the prototype, which ends up feeling like adding extension methods in C#.
The reason to add methods to the Array prototype is that it makes it easier to continue to chain calls together in the fluent style that started all this experimentation, e.g.
// union s += "<h1>chris's family</h1>" +
[chris].union(chris.children).map(function (p) { return p; }).join(", ");
Where Are We?
If you’re a JS programmer, it may be that you appreciate using it like a scripting language and so none of this “set-based” nonsense is important to you. That’s OK. JS is for everyone.
If you’re a C# programmer, you might dismiss JS as a “toy” language and turn your nose up at it. This would be a mistake. JS has a combination of ease-of-use for the non-programmer-programmer and raw power for the programmer-programmer that makes it worth taking seriously. Plus, with it’s popularity on the web, it’s hard to ignore.
If you’re a functional programmer, you look at all this set-based programming and say, “Duh. What took you so long?”
Me, I’m just happy I can program the way I like to in my new home on the web. : )