简体   繁体   中英

Javascript macro: implementing F# style forward pipe operator

I want to implement a higher order function ( hof ) that essentially works like F# style forward-pipe operator (passes a value as the first argument to another function, myFunc ). The only way I can think of is this:

function hof(val, myFunc, args_array) {...}

where args_array is the array of arguments for the call to myFunc (excluding the first argument, since that's going to be val )

But this doesn't look very elegant to me. Is there a better way to do this?

Edit: I found this on github https://gist.github.com/aaronpowell/d5ffaf78666f2b8fb033 . But I don't really understand what the sweet.js code is doing. It'd be very helpful if you could annotate the code, specifically:

case infix { $val | _ $fn($args (,) ...) } => {
    return #{
        ($fn.length <= [$args (,) ...].length + 1 ? $fn($args (,) ..., $val) : $fn.bind(null, $args (,) ..., $val))
    }
}

case infix { $val | _ $fn } => {
    return #{
        ($fn.length <= 1 ? $fn($val) : $fn.bind(null, $val))
    }
}

If you want something like the F# pipeline operator, I think your best bet is either this approach that you had in your post:

hof(val, myFunc, [arg1, arg2, arg3]);

or this:

hof(val, myFunc, arg1, arg2, arg3);

The first one can be implemented like this:

function hof(val, func, args) {
    func.apply(this, [val].concat(args || []));
}

The second one can be implemented like this:

function hof(val, func) {
    func.apply(this, [val].concat(Array.prototype.slice(arguments, 2));
}

But that all leaves the question of why you wouldn't just call the function in a normal way:

myFunc(val, arg1, arg2, arg3);

Isn't this called Currying ?

Anyhow, here's a rubbish example, I'm sure there are better examples if you search :

function myCurry() {
    var args = [].slice.call(arguments);
    var fn = args.splice(0,1)[0];
    return function(arg) {
      var a = [].concat(arg, args);
      return fn.apply(this, a);
    };
}

// Just sums the supplied arguments, initial set are 1,2,3
var fn = myCurry(function(){
                   var sum = 0;
                   for (var i=0, iLen=arguments.length; i<iLen; i++) {

                     // Show args in sequence - 4, 1, 2, 3
                     console.log('arguments ' + i + ': ' + arguments[i]);
                     sum += arguments[i];
                   }
                   return sum;
                }, 1,2,3);

// Provide an extra argument 4
console.log(fn(4)); // 10

Sweet.js macros are simple, they only look cryptic at first sight.

A forward pipe in F# language, infix operator |> applies function on the right to the result of the expression on the left.

New syntax should look like this( |> macro):

var seven = 7;
var outcome = 3 |> plus(seven) |> divideBy(2); // outcome must be 5

The pipe macro must append argument to the function, so that after expansion functions would look like this: plus(seven, 3) .

The sweet.js macro in the question works in simplest cases.

Main part is in this snippet return #{ /** macro expansion template */ } . It contains javascript that is executed in compile time(sweet.js) returning text that is a code for a macro expansion. The result of compilation:

var outcome$633 = plus.length <= [seven].length + 1 ? plus(seven, 5) : plus.bind(null, seven, 5);

The length of a function in js is its number of expected positional arguments .

A sofisticated macro that allows using functions contained in objects(dot notation):

macro (|>) {
  case infix { $val | _ $fn(.)... ( $args(,)... ) } => {
    return #{
      ($fn(.)....length <= [$args(,)...].length + 1 ? $fn(.)...( $args(,)..., $val) : $fn(.)....bind(null, $args(,)..., $val))
    }
  }

  case infix { $val | _ $fn($args ...) } => {
    return #{
      ($fn.length <= [$args ...].length + 1 ? $fn($args ..., $val) : $fn.bind(null, $args ..., $val))
    }
  }

  case infix { $val | _ $fn(.)... } => {
    return #{
      ($fn(.)....length <= 1 ? $fn(.)...($val) : $fn(.)....bind(null, $val))
    }
  }
}

One restriction/requirement of this macro is to omit parens () if a function in a pipeline does not take arguments.

Line var outcome = 5 |> plus(seven) |> obj.subObj.func; expands to this nested ternary operator expression in js:

var outcome$640 = obj.subObj.func.length <= 1 ? obj.subObj.func(plus.length <= [seven].length + 1 ? plus(seven, 5) : plus.bind(null, seven, 5)) : obj.subObj.func.bind(null, plus.length <= [seven].length + 1 ? plus(seven, 5) : plus.bind(null, seven, 5));

You can easily use a closure for this kind of wrapping:

function callf(f, count) {
    for (var i=0; i<count; i++) {
        f(i);
    }
}

function g(x, y) {
    console.log("called g(" + x + ", " + y + ")");
}

callf(function(x) { g(x, "hey!"); }, 10);

Have the first call to myFunc return another function that accepts a single parameter. Then, when you do your initial call hof(val, myFunc(arg1, arg2, arg3)); , that function is returned, and it only needs one more argument, val to finish executing, which is provided by hof .

function hof(val, func){
  func(val);
}

function myFunc(arg1, arg2, arg3){
  return function(val){
    // do something with val, arg1, arg2, arg3 here
  }
}

hof(val, myFunc(arg1, arg2, arg3));

Simple

If what you said is true, you could use the following code, but i don't see a point since you can run myFunc directly.

function myFunc(val, arg1, arg2, arg3){
  // do something
}

function hof(val, myfunc, arg1, arg2, arg3){
  return myFunc(val, arg1, arg2, arg3);
}

Curry

function customCurry(func){
  var args = [];
  var result;

  return function(){
    var l = arguments.length;

    for(var i = 0; i < l; i++){
        if(args.length > 3) break;
        args.push(arguments[i]);
    }

    if(args.length > 3){
        return result = func(args[3], args[0], args[1], args[2]);
    }
  }
}

var preloaded = customCurry(myFunc)(arg1, arg2, arg3);

hof(val, myFunc);

function hof(val, func){
    func(val);
}

ecma5 added Function.bind() , which also curries:

 myFunc2=myFunc.bind(this, val, arg1, arg2, arg3);

if you want a re-usable generic function to make bind() a little easier:

 function hof(f, args){ return f.bind.apply(f, [this].concat(args));}

which returns a new function, parameters pre-filled with the previously passed value(s). Passing more arguments will shift them to the right, and you can access all of them via the arguments keyword.

Let's use an example.

Suppose you had a simple function to sum up numbers:

var sum = function() {
    return Array.prototype.slice.call(arguments).reduce(function(sum, value) {
        return sum + value;
    }, 0);
};

You can't simply call it by passing the arguments in and then passing that to another function because it will evaluate the function, so we need to create a wrapper around the function to shift the arguments across:

var sumWrapper = function() {
    var args = Array.prototype.slice.call(arguments);
    return function() {
        var innerArgs = Array.prototype.slice.call(arguments).concat(args);
        return sum.apply(null, innerArgs);
    }
};

We can then create your higher order function (assume the last argument is the function in question):

var higherOrderSum = function() {
    return arguments[arguments.length - 1].apply(null, Array.prototype.slice.call(arguments, 0, arguments.length - 1));
};

Usage:

alert(higherOrderSum(3, 4, sumWrapper(1, 2, 3)));

在ES6中,这变得非常紧凑:

var hof = (fn, ...params) => (...addtl) => fn(...parms, ...addtl);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM