From d7ad2054c1b405c67aa852b517c2291011d3c638 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Fri, 6 Dec 2013 15:53:30 -0800 Subject: [PATCH] Bug 947501: Uplift Add-on SDK to Firefox. r=me https://github.com/mozilla/addon-sdk/compare/11344b2...dc6000a --- .../tutorials/reusable-modules.md | 8 +- .../doc/module-source/sdk/lang/functional.md | 349 +++++++++++++++--- addon-sdk/source/lib/sdk/event/target.js | 6 +- addon-sdk/source/lib/sdk/lang/functional.js | 242 +++++++++--- addon-sdk/source/test/test-functional.js | 251 +++++++++++-- 5 files changed, 719 insertions(+), 137 deletions(-) diff --git a/addon-sdk/source/doc/dev-guide-source/tutorials/reusable-modules.md b/addon-sdk/source/doc/dev-guide-source/tutorials/reusable-modules.md index 3be1f9ee7b5..5872dd7de82 100644 --- a/addon-sdk/source/doc/dev-guide-source/tutorials/reusable-modules.md +++ b/addon-sdk/source/doc/dev-guide-source/tutorials/reusable-modules.md @@ -35,13 +35,13 @@ geolocation API in Firefox. ## Using Geolocation in an Add-on ## Suppose we want to use the -[geolocation API built into Firefox](https://developer.mozilla.org/en/using_geolocation). +[geolocation API built into Firefox](https://developer.mozilla.org/en-US/docs/WebAPI/Using_geolocation). The SDK doesn't provide an API to access geolocation, but we can [access the underlying XPCOM API using `require("chrome")`](dev-guide/guides/xul-migration.html#xpcom). The following add-on adds a [button to the toolbar](dev-guide/tutorials/adding-toolbar-button.html): when the user clicks the button, it loads the -[XPCOM nsIDOMGeoGeolocation](https://developer.mozilla.org/en/XPCOM_Interface_Reference/NsIDOMGeoGeolocation) +[XPCOM nsIDOMGeoGeolocation](https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/NsIDOMGeoGeolocation) object, and retrieves the user's current position: var {Cc, Ci} = require("chrome"); @@ -88,7 +88,7 @@ info: longitude: 93.0785269 So far, so good. But the geolocation guide on MDN tells us that we must -[ask the user for permission](https://developer.mozilla.org/en/using_geolocation#Prompting_for_permission) +[ask the user for permission](https://developer.mozilla.org/en-US/docs/WebAPI/Using_geolocation#Prompting_for_permission) before using the API. So we'll extend the add-on to include an adapted version of the code in @@ -384,4 +384,4 @@ to add your name as the author, choose a distribution license, and so on. To see some of the modules people have already developed, see the page of [community-developed modules](https://github.com/mozilla/addon-sdk/wiki/Community-developed-modules). To learn how to use third-party modules in your own code, see the -[tutorial on adding menu items](dev-guide/tutorials/adding-menus.html). \ No newline at end of file +[tutorial on adding menu items](dev-guide/tutorials/adding-menus.html). diff --git a/addon-sdk/source/doc/module-source/sdk/lang/functional.md b/addon-sdk/source/doc/module-source/sdk/lang/functional.md index b9945103cb0..f71699bb970 100644 --- a/addon-sdk/source/doc/module-source/sdk/lang/functional.md +++ b/addon-sdk/source/doc/module-source/sdk/lang/functional.md @@ -13,15 +13,17 @@ When the method is invoked on an instance of the object, the original function is called. It is passed the object instance (i.e. `this`) as the first parameter, followed by any parameters passed into the method. - let { method } = require("sdk/lang/functional"); - let myNumber = { + const { method } = require("sdk/lang/functional"); + + const times = (target, x) => target.number *= x; + const add = (target, x) => target.number += x; + + const myNumber = { times: method(times), add: method(add), number: 0 }; - function times (target, x) { return target.number *= x; } - function add (target, x) { return target.number += x; } console.log(myNumber.number); // 0 myNumber.add(10); // 10 @@ -44,8 +46,8 @@ wait (i.e. `setTimeout(function () { ... }, 0)`), except that the wrapped functi may be reused and does not need to be repeated each time. This also enables you to use these functions as event listeners. - let { defer } = require("sdk/lang/functional"); - let fn = defer(function myEvent (event, value) { + const { defer } = require("sdk/lang/functional"); + const fn = defer((event, value) => { console.log(event + " : " + value); }); @@ -74,16 +76,11 @@ An alias for [defer](modules/sdk/lang/functional.html#defer(fn)). Invokes `callee`, passing `params` as an argument and `self` as `this`. Returns the value that is returned by `callee`. - let { invoke } = require("sdk/lang/functional"); + const { invoke } = require("sdk/lang/functional"); + const sum = (...args) => args.reduce((a, b) => a + b); invoke(sum, [1,2,3,4,5], null); // 15 - function sum () { - return Array.slice(arguments).reduce(function (a, b) { - return a + b; - }); - } - @param callee {function} Function to invoke. @param params {Array} @@ -98,9 +95,9 @@ Returns the value that is returned by `callee`. @function Takes a function and bind values to one or more arguments, returning a new function of smaller arity. - let { partial } = require("sdk/lang/functional"); - let add = function add (x, y) { return x + y; } - let addOne = partial(add, 1); + const { partial } = require("sdk/lang/functional"); + const add = (x, y) => x + y; + const addOne = partial(add, 1); addOne(5); // 6 addOne(10); // 11 @@ -122,14 +119,16 @@ Returns the [composition](http://en.wikipedia.org/wiki/Function_composition_(com return value of the function that follows. In math terms, composing the functions `f()`, `g()`, and `h()` produces `f(g(h()))`. - let { compose } = require("sdk/lang/functional"); + const { compose } = require("sdk/lang/functional"); - let welcome = compose(exclaim, greet); + const square = x => x * x; + const increment = x => x + 1; - welcome('moe'); // "hi: moe!"; + const f1 = compose(increment, square); + f1(5); // => 26 - function greet (name) { return "hi: " + name; } - function exclaim (statement) { return statement + "!"; } + const f2 = compose(square, increment); + f2(5); // => 36 @param fn... {function} Takes a variable number of functions as arguments and composes them from right to left. @@ -144,16 +143,14 @@ Returns the first function passed as an argument to the second, allowing you to adjust arguments, run code before and after, and conditionally execute the original function. - let { wrap } = require("sdk/lang/functional"); + const { wrap } = require("sdk/lang/functional"); - let wrappedHello = wrap(hello, function (fn, name) { - return "before, " + fn(name) + "after"; - }); + const hello = name => "hello: " + name; + const wrappedHello = wrap(hello, (fn, name) => + "before, " + fn(name) + "after"); wrappedHello("moe"); // "before, hello: moe, after" - function hello (name) { return "hello: " + name; } - @param fn {function} The function to be passed into the `wrapper` function. @@ -170,8 +167,8 @@ conditionally execute the original function. @function Returns the same value that is used as the argument. In math: f(x) = x. - let { identity } = require("sdk/lang/functional"); - let x = 5; + const { identity } = require("sdk/lang/functional"); + const x = 5; identity(x); // 5 @param value {mixed} @@ -190,15 +187,15 @@ storing the result, based on the arguments to the original function. The default `hashFunction` just uses the first argument to the memoized function as the key. - let { memoize } = require("sdk/lang/functional"); + const { memoize } = require("sdk/lang/functional"); - let memoizedFn = memoize(primeFactorization); + const memoizedFn = memoize(primeFactorization); memoizedFn(50); // Returns [2, 5, 5], had to compute memoizedFn(100); // Returns [2, 2, 5, 5], had to compute memoizedFn(50); // Returns [2, 5, 5] again, but pulled from cache - function primeFactorization (x) { + const primeFactorization = x => { // Some tricky stuff } @@ -209,16 +206,14 @@ the key. // function will just parse the last name, as our naive // implementation assumes that they will share the same lineage - let getLineage = memoize(function (name) { + const getLineage = memoize(name => { // computes lineage return data; }, hasher); // Hashing function takes a string of first and last name // and returns the last name. - function hasher (input) { - return input.split(" ")[1]; - } + const hasher = input => input.split(" ")[1]; getLineage("homer simpson"); // Computes and returns information for "simpson" getLineage("lisa simpson"); // Returns cached for "simpson" @@ -240,12 +235,11 @@ Much like `setTimeout`, `delay` invokes a function after waiting a set number of milliseconds. If you pass additional, optional, arguments, they will be forwarded on to the function when it is invoked. - let { delay } = require("sdk/lang/functional"); + const { delay } = require("sdk/lang/functional"); + const printAdd = (a, b) console.log(a + "+" + b + "=" + (a+b)); delay(printAdd, 2000, 5, 10); - // Prints "5+10=15" in two seconds (2000ms) - function printAdd (a, b) { console.log(a + "+" + b + "=" + (a+b)); } @param fn {function} A function to be delayed. @@ -264,8 +258,8 @@ Repeated calls to the modified function will have no effect, returning the value from the original call. Useful for initialization functions, instead of having to set a boolean flag and checking it later. - let { once } = require("sdk/lang/functional"); - let setup = once(function (env) { + const { once } = require("sdk/lang/functional"); + const setup = once(env => { // initializing important things console.log("successfully initialized " + env); return 1; // Assume success and return 1 @@ -273,7 +267,7 @@ of having to set a boolean flag and checking it later. setup('dev'); // returns 1 // prints "successfully initialized dev" - + // Future attempts to call this function just return the cached // value that was returned previously setup('production'); // Returns 1 @@ -286,16 +280,156 @@ of having to set a boolean flag and checking it later. The wrapped `fn` that can only be executed once. - + +@function +An alias for [once](modules/sdk/lang/functional.html#once(fn)). + + + +@function +Takes a `f` function and returns a function that takes the same +arguments as `f`, has the same effects, if any, and returns the +opposite truth value. + + const { complement } = require("sdk/lang/functional"); + + let isOdd = x => Boolean(x % 2); + + isOdd(1) // => true + isOdd(2) // => false + + let isEven = complement(isOdd); + + isEven(1) // => false + isEven(2) // => true + +@param lambda {function} + The function to compose from + +@returns {boolean} + `!lambda(...)` + + + +@function +Constructs function that returns `x` no matter what is it +invoked with. + + const { constant } = require("sdk/lang/functional"); + + const one = constant(1); + + one(); // => 1 + one(2); // => 1 + one.apply({}, 3); // => 1 + +@param x {object} + Value that will be returned by composed function + +@returns {function} + + + + +@function +Apply function that behaves like `apply` in other functional +languages: + + const { apply } = require("sdk/lang/functional"); + + const dashify = (...args) => args.join("-"); + + apply(dashify, 1, [2, 3]); // => "1-2-3" + apply(dashify, "a"); // => "a" + apply(dashify, ["a", "b"]); // => "a-b" + apply(dashify, ["a", "b"], "c"); // => "a,b-c" + apply(dashify, [1, 2], [3, 4]); // => "1,2-3-4" + +@param f {function} + function to be invoked + + + +@function +Returns function identical to given `f` but with flipped order +of arguments. + + const { flip } = require("sdk/lang/functional"); + + const append = (left, right) => left + " " + right; + const prepend = flip(append); + + append("hello", "world") // => "hello world" + prepend("hello", "world") // => "world hello" + +@param f {function} + function whose arguments should to be flipped + +@returns {function} + function with flipped arguments + + + +@function +Takes `p` predicate, `consequent` function and an optional +`alternate` function and composes function that returns +application of arguments over `consequent` if application over +`p` is `true` otherwise returns application over `alternate`. +If `alternate` is not a function returns `undefined`. + + const { when } = require("sdk/lang/functional"); + + function Point(x, y) { + this.x = x + this.y = y + } + + const isPoint = x => x instanceof Point; + + const inc = when(isPoint, ({x, y}) => new Point(x + 1, y + 1)); + + inc({}); // => undefined + inc(new Point(0, 0)); // => { x: 1, y: 1 } + + const axis = when(isPoint, + ({ x, y }) => [x, y], + _ => [0, 0]); + + axis(new Point(1, 4)); // => [1, 4] + axis({ foo: "bar" }); // => [0, 0] + +@param p {function} + predicate function whose return value determines to which + function be delegated control. + +@param consequent {function} + function to which arguments are applied if `predicate` returned + `true`. + +@param alternate {function} + function to which arguments are applied if `predicate` returned + `false`. + +@returns {object|string|number|function} + Return value from `consequent` if `p` returned `true` or return + value from `alternate` if `p` returned `false`. If `alternate` + is not provided and `p` returned `false` then `undefined` is + returned. + + + + @function Creates a version of the input function that will return `this`. - let { chain } = require("sdk/lang/functional"); + const { chainable } = require("sdk/lang/functional"); function Person (age) { this.age = age; } - Person.prototype.happyBirthday = chain(function () this.age++); + Person.prototype.happyBirthday = chainable(function() { + return this.age++ + }); - let person = new Person(30); + const person = new Person(30); person .happyBirthday() @@ -311,7 +445,126 @@ Creates a version of the input function that will return `this`. The wrapped function that executes `fn` and returns `this`. - + @function -An alias for [once](modules/sdk/lang/functional.html#once(fn)). + +Takes field `name` and `target` and returns value of that field. +If `target` is `null` or `undefined` it would be returned back +instead of attempt to access it's field. Function is implicitly +curried, this allows accessor function generation by calling it +with only `name` argument. + + const { field } = require("sdk/lang/functional"); + + field("x", { x: 1, y: 2}); // => 1 + field("x")({ x: 1 }); // => 1 + field("x", { y: 2 }); // => undefiend + + const getX = field("x"); + getX({ x: 1 }); // => 1 + getX({ y: 1 }); // => undefined + getX(null); // => null + +@param name {string} + Name of the field to be returned + +@param target {object} + Target to get a field by the given `name` from + +@returns {object|function|string|number|boolean} + Field value + + + +@function + +Takes `.` delimited string representing `path` to a nested field +and a `target` to get it from. For convinience function is +implicitly curried, there for accessors can be created by invoking +it with just a `path` argument. + + const { query } = require("sdk/lang/functional"); + + query("x", { x: 1, y: 2}); // => 1 + query("top.x", { x: 1 }); // => undefiend + query("top.x", { top: { x: 2 } }); // => 2 + + const topX = query("top.x"); + topX({ top: { x: 1 } }); // => 1 + topX({ y: 1 }); // => undefined + topX(null); // => null + +@param path {string} + `.` delimited path to a field + +@param target {object} + Target to get a field by the given `name` from + +@returns {object|function|string|number|boolean} + Field value + + + +@function + +Takes `Type` (constructor function) and a `value` and returns +`true` if `value` is instance of the given `Type`. Function is +implicitly curried this allows predicate generation by calling +function with just first argument. + + const { isInstance } = require("sdk/lang/functional"); + + function X() {} + function Y() {} + let isX = isInstance(X); + + isInstance(X, new X); // true + isInstance(X)(new X); // true + isInstance(X, new Y); // false + isInstance(X)(new Y); // false + + isX(new X); // true + isX(new Y); // false + +@param Type {function} + Type (constructor function) + +@param instance {object} + Instance to test + +@returns {boolean} + + + +@function + +Functions takes `expected` and `actual` values and returns `true` if +`expected === actual`. If invoked with just one argument returns pratially +applied function, which can be invoked to provide a second argument, this +is handy with `map`, `filter` and other high order functions: + + const { is } = require("sdk/util/oops"); + [ 1, 0, 1, 0, 1 ].map(is(1)) // => [ true, false, true, false, true ] + +@param expected {object|string|number|boolean} +@param actual {object|string|number|boolean} + +@returns {boolean} + + + +@function + +Functions takes `expected` and `actual` values and returns `true` if +`expected !== actual`. If invoked with just one argument returns pratially +applied function, which can be invoked with a second argument, which is +handy with `map`, `filter` and other high order functions: + + const { isnt } = require("sdk/util/oops"); + [ 1, 0, 1, 0, 1 ].map(isnt(0)) // => [ true, false, true, false, true ] + +@param expected {object|string|number|boolean} +@param actual {object|string|number|boolean} + +@returns {boolean} diff --git a/addon-sdk/source/lib/sdk/event/target.js b/addon-sdk/source/lib/sdk/event/target.js index f90bac09d23..40ddeaf59ed 100644 --- a/addon-sdk/source/lib/sdk/event/target.js +++ b/addon-sdk/source/lib/sdk/event/target.js @@ -9,7 +9,7 @@ module.metadata = { }; const { on, once, off, setListeners } = require('./core'); -const { method, chain } = require('../lang/functional'); +const { method, chainable } = require('../lang/functional'); const { Class } = require('../core/heritage'); /** @@ -43,7 +43,7 @@ const EventTarget = Class({ * console.log('data received: ' + data) * }) */ - on: chain(method(on)), + on: chainable(method(on)), /** * Registers an event `listener` that is called once the next time an event * of the specified `type` is emitted. @@ -52,7 +52,7 @@ const EventTarget = Class({ * @param {Function} listener * The listener function that processes the event. */ - once: chain(method(once)), + once: chainable(method(once)), /** * Removes an event `listener` for the given event `type`. * @param {String} type diff --git a/addon-sdk/source/lib/sdk/lang/functional.js b/addon-sdk/source/lib/sdk/lang/functional.js index bfc86305bd3..27036de16c9 100644 --- a/addon-sdk/source/lib/sdk/lang/functional.js +++ b/addon-sdk/source/lib/sdk/lang/functional.js @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -// Disclaimer: Most of the functions in this module implement APIs from +// Disclaimer: Some of the functions in this module implement APIs from // Jeremy Ashkenas's http://underscorejs.org/ library and all credits for // those goes to him. @@ -12,19 +12,33 @@ module.metadata = { "stability": "unstable" }; -const { setImmediate, setTimeout } = require("../timers"); const { deprecateFunction } = require("../util/deprecate"); +const { setImmediate, setTimeout } = require("../timers"); + +const arity = f => f.arity || f.length; + +const name = f => f.displayName || f.name; + +const derive = (f, source) => { + f.displayName = name(source); + f.arity = arity(source); + return f; +}; /** - * Takes `lambda` function and returns a method. When returned method is - * invoked it calls wrapped `lambda` and passes `this` as a first argument - * and given argument as rest. + * Takes variadic numeber of functions and returns composed one. + * Returned function pushes `this` pseudo-variable to the head + * of the passed arguments and invokes all the functions from + * left to right passing same arguments to them. Composite function + * returns return value of the right most funciton. */ -function method(lambda) { - return function method() { - return lambda.apply(null, [this].concat(Array.slice(arguments))); - } -} +const method = (...lambdas) => { + return function method(...args) { + args.unshift(this); + return lambdas.reduce((_, lambda) => lambda.apply(this, args), + void(0)); + }; +}; exports.method = method; /** @@ -34,24 +48,13 @@ exports.method = method; * function is reused, instead of creating a new one each time. This also allows * to use this functions as event listeners. */ -function defer(f) { - return function deferred() setImmediate(invoke, f, arguments, this); -} +const defer = f => derive(function(...args) { + setImmediate(invoke, f, args, this); +}, f); exports.defer = defer; // Exporting `remit` alias as `defer` may conflict with promises. exports.remit = defer; -/* - * Takes a funtion and returns a wrapped function that returns `this` - */ -function chain(f) { - return function chainable(...args) { - f.apply(this, args); - return this; - }; -} -exports.chain = chain; - /** * Invokes `callee` by passing `params` as an arguments and `self` as `this` * pseudo-variable. Returns value that is returned by a callee. @@ -62,7 +65,7 @@ exports.chain = chain; * @param {Object} self * Object to be passed as a `this` pseudo variable. */ -function invoke(callee, params, self) callee.apply(self, params); +const invoke = (callee, params, self) => callee.apply(self, params); exports.invoke = invoke; /** @@ -74,14 +77,16 @@ exports.invoke = invoke; * * @returns The new function with binded values */ -function partial(fn) { - if (typeof fn !== "function") - throw new TypeError(String(fn) + " is not a function"); +const partial = (f, ...curried) => { + if (typeof(f) !== "function") + throw new TypeError(String(f) + " is not a function"); - let args = Array.slice(arguments, 1); - - return function() fn.apply(this, args.concat(Array.slice(arguments))); -} + let fn = derive(function(...args) { + return f.apply(this, curried.concat(args)); + }, f); + fn.arity = arity(f) - curried.length; + return fn; +}; exports.partial = partial; /** @@ -98,12 +103,11 @@ exports.partial = partial; * console.log(sum(2, 2)) // 4 * console.log(sum(2)(4)) // 6 */ -var curry = new function() { - function currier(fn, arity, params) { +const curry = new function() { + const currier = (fn, arity, params) => { // Function either continues to curry arguments or executes function // if desired arguments have being collected. - return function curried() { - var input = Array.slice(arguments); + const curried = function(...input) { // Prepend all curried arguments to the given arguments. if (params) input.unshift.apply(input, params); // If expected number of arguments has being collected invoke fn, @@ -111,11 +115,12 @@ var curry = new function() { return (input.length >= arity) ? fn.apply(this, input) : currier(fn, arity, input); }; - } + curried.arity = arity - (params ? params.length : 0); - return function curry(fn) { - return currier(fn, fn.length); - } + return curried; + }; + + return fn => currier(fn, arity(fn)); }; exports.curry = curry; @@ -131,12 +136,12 @@ exports.curry = curry; * * welcome('moe'); // => 'hi: moe!' */ -function compose() { - let lambdas = Array.slice(arguments); - return function composed() { - let args = Array.slice(arguments), index = lambdas.length; +function compose(...lambdas) { + return function composed(...args) { + let index = lambdas.length; while (0 <= --index) - args = [ lambdas[index].apply(this, args) ]; + args = [lambdas[index].apply(this, args)]; + return args[0]; }; } @@ -155,16 +160,15 @@ exports.compose = compose; * * hello(); // => 'before, hello: moe, after' */ -function wrap(f, wrapper) { - return function wrapped() - wrapper.apply(this, [ f ].concat(Array.slice(arguments))) -}; +const wrap = (f, wrapper) => derive(function wrapped(...args) { + return wrapper.apply(this, [f].concat(args)); +}, f); exports.wrap = wrap; /** * Returns the same value that is used as the argument. In math: f(x) = x */ -function identity(value) value +const identity = value => value; exports.identity = identity; /** @@ -174,14 +178,25 @@ exports.identity = identity; * the arguments to the original function. The default hashFunction just uses * the first argument to the memoized function as the key. */ -function memoize(f, hasher) { +const memoize = (f, hasher) => { let memo = Object.create(null); + let cache = new WeakMap(); hasher = hasher || identity; - return function memoizer() { - let key = hasher.apply(this, arguments); - return key in memo ? memo[key] : (memo[key] = f.apply(this, arguments)); - }; -} + return derive(function memoizer(...args) { + const key = hasher.apply(this, args); + const type = typeof(key); + if (key && (type === "object" || type === "function")) { + if (!cache.has(key)) + cache.set(key, f.apply(this, args)); + return cache.get(key); + } + else { + if (!(key in memo)) + memo[key] = f.apply(this, args); + return memo[key]; + } + }, f); +}; exports.memoize = memoize; /** @@ -189,9 +204,8 @@ exports.memoize = memoize; * the optional arguments, they will be forwarded on to the function when it is * invoked. */ -function delay(f, ms) { - let args = Array.slice(arguments, 2); - setTimeout(function(context) { return f.apply(context, args); }, ms, this); +const delay = function delay(f, ms, ...args) { + setTimeout(() => f.apply(this, args), ms); }; exports.delay = delay; @@ -201,10 +215,116 @@ exports.delay = delay; * the original call. Useful for initialization functions, instead of having to * set a boolean flag and then check it later. */ -function once(f) { +const once = f => { let ran = false, cache; - return function() ran ? cache : (ran = true, cache = f.apply(this, arguments)) + return derive(function(...args) { + return ran ? cache : (ran = true, cache = f.apply(this, args)); + }, f); }; exports.once = once; // export cache as once will may be conflicting with event once a lot. exports.cache = once; + +// Takes a `f` function and returns a function that takes the same +// arguments as `f`, has the same effects, if any, and returns the +// opposite truth value. +const complement = f => derive(function(...args) { + return args.length < arity(f) ? complement(partial(f, ...args)) : + !f.apply(this, args); +}, f); +exports.complement = complement; + +// Constructs function that returns `x` no matter what is it +// invoked with. +const constant = x => _ => x; +exports.constant = constant; + +// Takes `p` predicate, `consequent` function and an optional +// `alternate` function and composes function that returns +// application of arguments over `consequent` if application over +// `p` is `true` otherwise returns application over `alternate`. +// If `alternate` is not a function returns `undefined`. +const when = (p, consequent, alternate) => { + if (typeof(alternate) !== "function" && alternate !== void(0)) + throw TypeError("alternate must be a function"); + if (typeof(consequent) !== "function") + throw TypeError("consequent must be a function"); + + return function(...args) { + return p.apply(this, args) ? + consequent.apply(this, args) : + alternate && alternate.apply(this, args); + }; +}; +exports.when = when; + +// Apply function that behaves as `apply` does in lisp: +// apply(f, x, [y, z]) => f.apply(f, [x, y, z]) +// apply(f, x) => f.apply(f, [x]) +const apply = (f, ...rest) => f.apply(f, rest.concat(rest.pop())); +exports.apply = apply; + +// Returns function identical to given `f` but with flipped order +// of arguments. +const flip = f => derive(function(...args) { + return f.apply(this, args.reverse()); +}, f); +exports.flip = flip; + + +// Takes field `name` and `target` and returns value of that field. +// If `target` is `null` or `undefined` it would be returned back +// instead of attempt to access it's field. Function is implicitly +// curried, this allows accessor function generation by calling it +// with only `name` argument. +const field = curry((name, target) => + // Note: Permisive `==` is intentional. + target == null ? target : target[name]); +exports.field = field; + +// Takes `.` delimited string representing `path` to a nested field +// and a `target` to get it from. For convinience function is +// implicitly curried, there for accessors can be created by invoking +// it with just a `path` argument. +const query = curry((path, target) => { + const names = path.split("."); + const count = names.length; + let index = 0; + let result = target; + // Note: Permisive `!=` is intentional. + while (result != null && index < count) { + result = result[names[index]]; + index = index + 1; + } + return result; +}); +exports.query = query; + +// Takes `Type` (constructor function) and a `value` and returns +// `true` if `value` is instance of the given `Type`. Function is +// implicitly curried this allows predicate generation by calling +// function with just first argument. +const isInstance = curry((Type, value) => value instanceof Type); +exports.isInstance = isInstance; + +/* + * Takes a funtion and returns a wrapped function that returns `this` + */ +const chainable = f => derive(function(...args) { + f.apply(this, args); + return this; +}, f); +exports.chainable = chainable; +exports.chain = + deprecateFunction(chainable, "Function `chain` was renamed to `chainable`"); + +// Functions takes `expected` and `actual` values and returns `true` if +// `expected === actual`. Returns curried function if called with less then +// two arguments. +// +// [ 1, 0, 1, 0, 1 ].map(is(1)) // => [ true, false, true, false, true ] +const is = curry((expected, actual) => actual === expected); +exports.is = is; + +const isnt = complement(is); +exports.isnt = isnt; diff --git a/addon-sdk/source/test/test-functional.js b/addon-sdk/source/test/test-functional.js index 61cc79305c3..84c587fc36d 100644 --- a/addon-sdk/source/test/test-functional.js +++ b/addon-sdk/source/test/test-functional.js @@ -1,21 +1,22 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - +"use strict"; const { setTimeout } = require('sdk/timers'); const utils = require('sdk/lang/functional'); -const { invoke, defer, partial, compose, memoize, once, delay, wrap, curry, chain } = utils; +const { invoke, defer, partial, compose, memoize, once, is, isnt, + delay, wrap, curry, chainable, field, query, isInstance } = utils; const { LoaderWithHookedConsole } = require('sdk/test/loader'); exports['test forwardApply'] = function(assert) { - function sum(b, c) this.a + b + c + function sum(b, c) { return this.a + b + c; } assert.equal(invoke(sum, [2, 3], { a: 1 }), 6, 'passed arguments and pseoude-variable are used'); assert.equal(invoke(sum.bind({ a: 2 }), [2, 3], { a: 1 }), 7, 'bounded `this` pseoudo variable is used'); -} +}; exports['test deferred function'] = function(assert, done) { let nextTurn = false; @@ -26,13 +27,13 @@ exports['test deferred function'] = function(assert, done) { done(); } - let fixture = { a: 1, method: defer(sum) } + let fixture = { a: 1, method: defer(sum) }; fixture.method(2, 3); nextTurn = true; }; exports['test partial function'] = function(assert) { - function sum(b, c) this.a + b + c; + function sum(b, c) { return this.a + b + c; } let foo = { a : 5 }; @@ -68,11 +69,11 @@ exports['test compose'] = function(assert) { let target = { name: 'Joe', greet: compose(function exclaim(sentence) { - return sentence + '!' + return sentence + '!'; }, function(title) { return 'hi : ' + title + ' ' + this.name; }) - } + }; assert.equal(target.greet('Mr'), 'hi : Mr Joe!', 'this can be passed in'); @@ -106,7 +107,7 @@ exports['test wrap'] = function(assert) { assert.equal(target.hi(), 'Hello Matteo', 'works with this'); - function noop() { }; + function noop() { } let wrapped = wrap(noop, function(f) { return Array.slice(arguments); }); @@ -117,7 +118,7 @@ exports['test wrap'] = function(assert) { }; exports['test memoize'] = function(assert) { - function fib(n) n < 2 ? n : fib(n - 1) + fib(n - 2) + const fib = n => n < 2 ? n : fib(n - 1) + fib(n - 2); let fibnitro = memoize(fib); assert.equal(fib(10), 55, @@ -125,7 +126,7 @@ exports['test memoize'] = function(assert) { assert.equal(fibnitro(10), 55, 'a memoized version of fibonacci produces identical results'); - function o(key, value) { return value; }; + function o(key, value) { return value; } let oo = memoize(o), v1 = {}, v2 = {}; @@ -136,12 +137,12 @@ exports['test memoize'] = function(assert) { assert.notEqual(oo(1), oo(2), 'values do not override'); assert.equal(o(3, v2), oo(2, 3), 'returns same value as un-memoized'); - let get = memoize(function(attribute) this[attribute]) - let target = { name: 'Bob', get: get } + let get = memoize(function(attribute) { return this[attribute]; }); + let target = { name: 'Bob', get: get }; assert.equal(target.get('name'), 'Bob', 'has correct `this`'); assert.equal(target.get.call({ name: 'Jack' }, 'name'), 'Bob', - 'name is memoized') + 'name is memoized'); assert.equal(get('name'), 'Bob', 'once memoized can be called without this'); }; @@ -155,13 +156,13 @@ exports['test delay'] = function(assert, done) { }; exports['test delay with this'] = function(assert, done) { - let context = {} + let context = {}; delay.call(context, function(name) { assert.equal(this, context, 'this was passed in'); assert.equal(name, 'Tom', 'argument was passed in'); done(); }, 10, 'Tom'); -} +}; exports['test once'] = function(assert) { let n = 0; @@ -172,7 +173,12 @@ exports['test once'] = function(assert) { assert.equal(n, 1, 'only incremented once'); - let target = { state: 0, update: once(function() this.state ++ ) }; + let target = { + state: 0, + update: once(function() { + return this.state ++; + }) + }; target.update(); target.update(); @@ -182,7 +188,7 @@ exports['test once'] = function(assert) { exports['test once with argument'] = function(assert) { let n = 0; - let increment = once(function(a) n++); + let increment = once(a => n++); increment(); increment('foo'); @@ -195,11 +201,121 @@ exports['test once with argument'] = function(assert) { assert.equal(n, 1, 'only incremented once'); }; -exports['test chain'] = function (assert) { +exports['test complement'] = assert => { + let { complement } = require("sdk/lang/functional"); + + let isOdd = x => Boolean(x % 2); + + assert.equal(isOdd(1), true); + assert.equal(isOdd(2), false); + + let isEven = complement(isOdd); + + assert.equal(isEven(1), false); + assert.equal(isEven(2), true); + + let foo = {}; + let isFoo = function() { return this === foo; }; + let insntFoo = complement(isFoo); + + assert.equal(insntFoo.call(foo), false); + assert.equal(insntFoo.call({}), true); +}; + +exports['test constant'] = assert => { + let { constant } = require("sdk/lang/functional"); + + let one = constant(1); + + assert.equal(one(1), 1); + assert.equal(one(2), 1); +}; + +exports['test apply'] = assert => { + let { apply } = require("sdk/lang/functional"); + + let dashify = (...args) => args.join("-"); + + assert.equal(apply(dashify, 1, [2, 3]), "1-2-3"); + assert.equal(apply(dashify, "a"), "a"); + assert.equal(apply(dashify, ["a", "b"]), "a-b"); + assert.equal(apply(dashify, ["a", "b"], "c"), "a,b-c"); + assert.equal(apply(dashify, [1, 2], [3, 4]), "1,2-3-4"); +}; + +exports['test flip'] = assert => { + let { flip } = require("sdk/lang/functional"); + + let append = (left, right) => left + " " + right; + let prepend = flip(append); + + assert.equal(append("hello", "world"), "hello world"); + assert.equal(prepend("hello", "world"), "world hello"); + + let wrap = function(left, right) { + return left + " " + this + " " + right; + }; + let invertWrap = flip(wrap); + + assert.equal(wrap.call("@", "hello", "world"), "hello @ world"); + assert.equal(invertWrap.call("@", "hello", "world"), "world @ hello"); + + let reverse = flip((...args) => args); + + assert.deepEqual(reverse(1, 2, 3, 4), [4, 3, 2, 1]); + assert.deepEqual(reverse(1), [1]); + assert.deepEqual(reverse(), []); + + // currying still works + let prependr = curry(prepend); + + assert.equal(prependr("hello", "world"), "world hello"); + assert.equal(prependr("hello")("world"), "world hello"); +}; + +exports["test when"] = assert => { + let { when } = require("sdk/lang/functional"); + + let areNums = (...xs) => xs.every(x => typeof(x) === "number"); + + let sum = when(areNums, (...xs) => xs.reduce((y, x) => x + y, 0)); + + assert.equal(sum(1, 2, 3), 6); + assert.equal(sum(1, 2, "3"), undefined); + + let multiply = when(areNums, + (...xs) => xs.reduce((y, x) => x * y, 1), + (...xs) => xs); + + assert.equal(multiply(2), 2); + assert.equal(multiply(2, 3), 6); + assert.deepEqual(multiply(2, "4"), [2, "4"]); + + function Point(x, y) { + this.x = x; + this.y = y; + } + + let isPoint = x => x instanceof Point; + + let inc = when(isPoint, ({x, y}) => new Point(x + 1, y + 1)); + + assert.equal(inc({}), undefined); + assert.deepEqual(inc(new Point(0, 0)), { x: 1, y: 1 }); + + let axis = when(isPoint, + ({ x, y }) => [x, y], + _ => [0, 0]); + + assert.deepEqual(axis(new Point(1, 4)), [1, 4]); + assert.deepEqual(axis({ foo: "bar" }), [0, 0]); +}; + +exports["test chainable"] = function(assert) { let Player = function () { this.volume = 5; }; Player.prototype = { - setBand: chain(function (band) this.band = band), - incVolume: chain(function () this.volume++) + setBand: chainable(function (band) { return (this.band = band); }), + incVolume: chainable(function () { return this.volume++; }) }; let player = new Player(); player @@ -210,4 +326,97 @@ exports['test chain'] = function (assert) { assert.equal(player.volume, 11, 'accepts no arguments in chain'); }; +exports["test field"] = assert => { + let Num = field("constructor", 0); + assert.equal(Num.name, Number.name); + assert.ok(typeof(Num), "function"); + + let x = field("x"); + + [ + [field("foo", { foo: 1 }), 1], + [field("foo")({ foo: 1 }), 1], + [field("bar", {}), undefined], + [field("bar")({}), undefined], + [field("hey", undefined), undefined], + [field("hey")(undefined), undefined], + [field("how", null), null], + [field("how")(null), null], + [x(1), undefined], + [x(undefined), undefined], + [x(null), null], + [x({ x: 1 }), 1], + [x({ x: 2 }), 2], + ].forEach(([actual, expected]) => assert.equal(actual, expected)); +}; + +exports["test query"] = assert => { + let Num = query("constructor", 0); + assert.equal(Num.name, Number.name); + assert.ok(typeof(Num), "function"); + + let x = query("x"); + let xy = query("x.y"); + + [ + [query("foo", { foo: 1 }), 1], + [query("foo")({ foo: 1 }), 1], + [query("foo.bar", { foo: { bar: 2 } }), 2], + [query("foo.bar")({ foo: { bar: 2 } }), 2], + [query("foo.bar", { foo: 1 }), undefined], + [query("foo.bar")({ foo: 1 }), undefined], + [x(1), undefined], + [x(undefined), undefined], + [x(null), null], + [x({ x: 1 }), 1], + [x({ x: 2 }), 2], + [xy(1), undefined], + [xy(undefined), undefined], + [xy(null), null], + [xy({ x: 1 }), undefined], + [xy({ x: 2 }), undefined], + [xy({ x: { y: 1 } }), 1], + [xy({ x: { y: 2 } }), 2] + ].forEach(([actual, expected]) => assert.equal(actual, expected)); +}; + +exports["test isInstance"] = assert => { + function X() {} + function Y() {} + let isX = isInstance(X); + + [ + isInstance(X, new X()), + isInstance(X)(new X()), + !isInstance(X, new Y()), + !isInstance(X)(new Y()), + isX(new X()), + !isX(new Y()) + ].forEach(x => assert.ok(x)); +}; + +exports["test is"] = assert => { + + assert.deepEqual([ 1, 0, 1, 0, 1 ].map(is(1)), + [ true, false, true, false, true ], + "is can be partially applied"); + + assert.ok(is(1, 1)); + assert.ok(!is({}, {})); + assert.ok(is()(1)()(1), "is is curried"); + assert.ok(!is()(1)()(2)); +}; + +exports["test isnt"] = assert => { + + assert.deepEqual([ 1, 0, 1, 0, 1 ].map(isnt(0)), + [ true, false, true, false, true ], + "is can be partially applied"); + + assert.ok(!isnt(1, 1)); + assert.ok(isnt({}, {})); + assert.ok(!isnt()(1)()(1)); + assert.ok(isnt()(1)()(2)); +}; + require('test').run(exports);