понедельник, 17 октября 2011 г.

Simple objects dynamic constructors (Array \ Arguments ⇒ Object)

In the previous article I described function that gives you function overloading ability in JavaScript. But in the end it become a bit Javash. Anyway Than I needed to created some other object with different keys, like:
{a1:1, a2:2, a3:3}
{alpha:10, beta:20, gamma:30}
So I made this:
function dynConstructor(){
  var varNames = Array.prototype.slice.call(arguments);
  return function(){
    var values = Array.prototype.slice.call(arguments);
    if(Array.isArray(values[0])){
      values = values[0];
    }
    var map = {};
    varNames.slice(0, values.length).forEach(function(varName, i){
      map[varName] = values[i];
    });
    return map;
  }
}
This function makes other functions given list of keys, which will be merged with values, than you put into obtained function as parameters. It's more clear with examples:
var pointVector = dynConstructor('x', 'y', 'z');
var displaceVector = dynConstructor('a1', 'a2', 'a3');
var anglesVector = dynConstructor('a', 'b', 'c');

pointVector(10, 20, 30) == {x:10, y:20, z:30};
displaceVector(1, 2, 3) == {a1:1, a2:2, a3:3};
anglesVector(120, 90, 90) == {a:120, b:90, c:90};

pointVector([1, 2, 3]) == {x:1, y:2, z:3}; //You can pass an array also
Also you can use it for creating any simple constructors. Like:
var person = dynConstructor('Name', 'Age', 'Sex');

person('Aleksandr', 27, 'M') == {Name: 'Aleksandr', Age: 27, Sex: 'M'};
Btw, do you know that you can use UTF-8 symbols related to specific domain for variable? =) Like this:
var displaceVector = dynConstructor('a¹', 'a²', 'a³');
var anglesVector = dynConstructor('ɑ', 'β', 'ɣ');

displaceVector(1,2,3) == {'a¹':1, 'a²':2, 'a³':3};
anglesVector(120, 90, 90) == {ɑ:120, β:90, ɣ:90};

суббота, 15 октября 2011 г.

Overloading in JavaScript

I wanted to make simple JavaScript method, that returns a Point object with coordinates. Something like this:
  
function point (x, y, z) {
  return {x:x, y:y, z:z};
}

But sometimes I had to use it as follows:
point.apply(null, xyz); //Where xyz == [x,y,z]
But I hate to use `.apply` or `.call` methods too often, so I wanted to make overloaded method so it can handles 1 and 3 arguments. Most straight forward way may looks like this:
function point() {
  if(arguments.length==1){
    return {x:arguments[0][0], y:arguments[0][1], z:arguments[0][2]};
  }else if (arguments.length==3){
    return {x:arguments[0], y:arguments[1], z:arguments[2]};
  }
}
which I don't like at all. It's looks ugly. So I came with this thing:
function point() {
  return ({
    1: function(xyz) {
      return point.apply(null, xyz);
    },
    3: function (x, y, z) {
      return {x:x, y:y, z:z};
    }
  }[arguments.length]).apply(null, arguments);
}

Here I use map with keys as number of arguments for each "overloaded" function. And if number of arguments passed in is satisfies on of those functions, than arguments apply to this overloaded function. Also see how first function calls itself recursively, so that if first argument is array with 3 items, than second overloaded function will be called eventually. Now I can easily add one more overloaded method:
function point() {
  return ({
    1: function(xyz) {
      return point.apply(null, xyz);
    },
   2: function(x, y) {
      return {x:x, y:y};
    },
    3: function (x, y, z) {
      return {x:x, y:y, z:z};
    }
  }[arguments.length]).apply(null, arguments);
}

But what if user use this function with number of arguments, that I don't support? It will die with some non-informative error. Let's add some checks:
function point() {
function point() {
  var methods = {
    1: function(xyz) {
      if(Array.isArray(xyz)){
        return point.apply(null, xyz);
      }else{
        throw "In one-argument case it should be an Array";
      }
    },
    2: function(x, y) {
      return {x:x, y:y};
    },
    3: function (x, y, z) {
      return {x:x, y:y, z:z};
    }
  }, argLen = arguments.length;

  if(methods[argLen]){
    return (methods[argLen]).apply(null, arguments);
  }
  
  throw "Unsupported number of arguments";
}

Ok. Now it will throw Exception in all cases except those which it can handle. Also we can rid of those explicitly defined keys, which mean number of an argument of each overloaded function. Function object have property .length. It returns number of defined arguments. So let's use it:
function point() {
  var methods = [
    function(xyz) {
      if(Array.isArray(xyz)){
        return point.apply(null, xyz);
      }else{
        throw "In one-argument case it should be an Array";
      }
    },
    function(x, y) {
      return {x:x, y:y};
    },
    function (x, y, z) {
      return {x:x, y:y, z:z};
    }
  ], argLen = arguments.length;
  
  for(var i in methods){
    if(methods[i].length == argLen){
      return methods[i].apply(null, arguments);
    }
  }

  throw "Unsupported number of arguments";
}

But now code become much bigger than it was in first snippet. So, I decoupled business logic from all this wrapper stuff:
var point = overload([
  function(xyz) {
    if(Array.isArray(xyz)){
      return point.apply(null, xyz);
    }else{
      throw "In one-argument case it should be an Array";
    }
  },
  function(x, y) {
    return {x:x, y:y};
  },
  function (x, y, z) {
    return {x:x, y:y, z:z};
  }
]);

function overload(methods) {
  return function(){
    var argLen = arguments.length;
    for(var i in methods){
      if(methods[i].length == argLen){
        return methods[i].apply(null, arguments);
      }
    }
    throw "Unsupported number of arguments";
  }
}

Logic from first overloaded function, which distribute array as arguments, also can be extracted into wrapper (with option for using it):
var point = overload([
  function(x, y) {
    return {x:x, y:y};
  },
  function (x, y, z) {
    return {x:x, y:y, z:z};
  }
], true);

function overload(methods, distrFirstArray) {
  return function(){
    var args = arguments;

    if(distrFirstArray && args.length == 1){
      if(Array.isArray(args[0])){
        args = args[0];
      } else {
        throw "In one-argument case it should be an Array";
      }
    }

    for(var i in methods){
      if(methods[i].length == args.length){
        return methods[i].apply(null, args);
      }
    }
    throw "Unsupported number of arguments";
  }
}

And finally I can improve it by rid of need to create array to pass my overloaded functions:
var point = overload(
    function(x, y) {
      return {x:x, y:y};
    },
    function (x, y, z) {
      return {x:x, y:y, z:z};
    },
    true
);
  
function overload() {
  var methods = Array.prototype.slice.call(arguments),
    distrFirstArray = typeof methods[methods.length - 1] == "boolean"?methods.pop():false;
  
  return function(){
    var args = arguments;
    if(distrFirstArray && args.length == 1){
      if(Array.isArray(args[0])){
        args = args[0];
      } else {
        throw "In one-argument case it should be an Array";
      }
    }

    for(var i in methods){
      if(methods[i].length == args.length){
        return methods[i].apply(null, args);
      }
    }
    throw "Unsupported number of arguments";
  }
}

As you can see now you can put any function with different number of arguments into `overload` method and get nice overloaded method, that will handle all overloading logic. But! if you need to use overloading in case of optional arguments with default value, this method is not for you.