Work around a Safari JIT bug.
[yuu.git] / src / yuu / yf.js
1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license http://www.gnu.org/licenses/gpl-2.0.html
4 @source: http://yukkurigames.com/yuu/
5 */
6
7 (function () {
8 "use strict";
9
10 /** yf - yuu foundation
11
12 A collection of tools for functional programming and the kind
13 of sequence (array) manipulations usually associated with
14 functional programming.
15
16 yf doesn't depend on yuu specifically, but may use standard
17 features not yet widely supported provided by yuu/pre.
18 */
19
20 function isFunction (o) {
21 /** Check if a value is a function */
22 return Object.prototype.toString.call(o) === '[object Function]';
23 }
24
25 function isString (s) {
26 /** Check if a value is a string */
27 return Object.prototype.toString.call(s) === "[object String]";
28 }
29
30 function I (x) { return x; }
31
32 function K (x) { return function () { return x; }; }
33
34 /** Get the minimum or maximum of two objects
35
36 The default JavaScript Math.min and Math.max are unsuitable as
37 generic functions. They don't work on strings. Because they
38 support a variable numbers of arguments they don't work
39 correctly when passed directly to higher-order functions that
40 pass "bonus" arguments such as Array.prototype.reduce.
41
42 These functions work on any objects comparable with < and >,
43 and only consider two arguments.
44 */
45 function min (a, b) { return b < a ? b : a; }
46 function max (a, b) { return b > a ? b : a; }
47 function clamp(x, lo, hi) { return max(lo, min(hi, x)); }
48
49 /** Get a property from the this object by name */
50 function getter (key) { return this[key]; }
51
52 /** Set a property value on the this object by name */
53 function setter (key, value) { this[key] = value; }
54
55 function unbind (f) {
56 /** Factor out the special 'this' argument from a function
57
58 unbind(f)(a, b, c) is equivalent to f.call(a, b, c), and
59 you may store the return value for later use without
60 storing the original function.
61 */
62
63 // return f.call.bind(f) is ~15% slower than this closure on
64 // Chrome 31 and negligibly slower on Firefox 25.
65 return function () {
66 return f.call.apply(f, arguments);
67 };
68 }
69
70 /** Unbound versions of useful functions */
71 var slice = unbind(Array.prototype.slice);
72
73 function contains (seq, o) {
74 return Array.prototype.indexOf.call(seq, o) !== -1;
75 }
76
77 function lacks (seq, o) {
78 return !contains(seq, o);
79 }
80
81 /** The first element of a sequence */
82 function head (a) { return a[0]; }
83
84 /** All but the first element of a sequence */
85 function tail (a) { return slice(a, 1); }
86
87 /** The last element of a sequence */
88 function last (a) { return a[a.length - 1]; }
89
90 /** All but the last element of a sequence */
91 function most (a) { return slice(a, 0, -1); }
92
93 /** The length of a sequence */
94 function len (seq) { return seq.length; }
95
96 /** Note that yf's argument order is slightly different from
97 JavaScript's normal order (but the same as most other
98 languages with functional elements).
99
100 This is irrelevant for `fold` and `filter`, but `each` and
101 `map` can take multiple sequences to allow callback functions
102 with arbitrary arity.
103
104 Because of this difference, there is no final `thisArg` as in
105 e.g. the standard Array's forEach. Instead the `this` of
106 the original call is forwarded. For example,
107 someArray.forEach(callback, thisArg)
108 becomes
109 yf.each.call(thisArg, callback, someArray)
110 */
111
112 function foldl (callback, seq, value) {
113 /** Like Array's reduce, but with no extra arguments
114
115 This traverses the sequence in indexed order, low-to-high,
116 and calls the provided function with the accumulated value
117 and the sequence item.
118 */
119 var i = 0;
120 if (arguments.length === 2)
121 value = seq[i++];
122 for (; i < seq.length; ++i)
123 value = callback.call(this, value, seq[i]);
124 return value;
125 }
126
127 function foldr (callback, seq, value) {
128 /** Like foldl, but reversed.
129
130 This traverses the sequence in indexed order, high-to-low,
131 and calls the provided function with the sequence item and
132 the accumulated value.
133 */
134 var i = seq.length - 1;
135 if (arguments.length === 2)
136 value = seq[i--];
137 for (; i >= 0; --i)
138 value = callback.call(this, seq[i], value);
139 return value;
140 }
141
142 function call (f, x) { return f.call(this, x); }
143
144 function compose () {
145 /** Create a function by composing multiple functions
146
147 compose(f, g, h)(x, y, z) is equivalent to f(g(h(x, y, z))).
148 */
149 var inner = last(arguments);
150 var funcs = most(arguments);
151 return function () {
152 return foldr.call(this, call, funcs, inner.apply(this, arguments));
153 };
154 }
155
156 // Generating specialized functions for plucking the nth argument
157 // and passing it to a callback is 100-120x faster than the
158 // generic one in V8.
159 //
160 // So generate specialized functions for small argc, and only
161 // resort to the generic approach when there's many sequences.
162 var CALLS = [];
163
164 /* jshint -W054*/ /* Function constructor is a form of eval */
165 for (var args = []; args.length < 32;
166 args.push(", seqs[" + args.length + "][i]"))
167 CALLS[args.length] = new Function(
168 "callback", "i", "seqs",
169 "return callback.call(this" + args.join("") + ");");
170
171 function lookup (prop) {
172 /** Return a function that gets a particular property */
173
174 // Building this as a closure is ~2-5x faster than writing
175 // a function lookup(prop, o) and binding it.
176 return function (s) { return s[prop]; };
177 }
178
179 function callx (callback, i, seqs) {
180 return callback.apply(this, seqs.map(lookup(i)));
181 }
182
183 function calln (argc) {
184 return CALLS[argc] || callx;
185 }
186
187 function argv (f) {
188 /** Provide a variadic-like version of f
189
190 Arguments passed to the function including and after the
191 final named one are collected and passed as a single
192 sequence (not necessarily Array) value.
193 */
194 var length = f.length;
195 switch (length) {
196 case 0: throw new Error("don't use argv for 0-ary functions");
197 case 1: return function () { return f.call(this, arguments); };
198 default:
199 return function () {
200 /* The following code can do this without creating a
201 new array, but it causes function calls to randomly
202 not happen in Safari (Mobile and OS X) - presumably
203 due to some JIT bug, because surrounding it in
204 an empty try/catch block also makes it work.
205
206 arguments[length - 1] = slice(arguments, length - 1);
207 */
208 var args = slice(arguments, 0, length - 1);
209 args.push(slice(arguments, length - 1));
210 return f.apply(this, args);
211 };
212 }
213 }
214
215 var each = argv(function (callback, seqs) {
216 /** Call a function on the given sequences' elements
217
218 If one sequence is longer than the others, `undefined`
219 will be passed for that sequence's argument.
220
221 For example, each(f, [a, b, c], [x, y]) is equivalent to
222 f(a, x);
223 f(b, y);
224 f(c, undefined);
225
226 If you want to pass a thisArg to the callback, use
227 each.call(thisArg, callback, ...).
228 */
229 var length = Math.max.apply(Math, seqs.map(len));
230 var fn = calln(seqs.length);
231 for (var i = 0; i < length; ++i)
232 fn.call(this, callback, i, seqs);
233 });
234
235 var eachr = argv(function (callback, seqs) {
236 /** Call a function on the given sequences' elements, backwards
237
238 If one sequence is longer than the others, `undefined` will be
239 passed for that sequence's argument.
240
241 For example, each(f, [a, b, c], [x, y]) is equivalent to
242 f(c, undefined);
243 f(b, y);
244 f(a, x);
245
246 If you want to pass a thisArg to the callback, use
247 eachr.call(thisArg, callback, ...).
248 */
249 var length = Math.max.apply(Math, seqs.map(len));
250 var fn = calln(seqs.length);
251 for (var i = length - 1; i >= 0; --i)
252 fn.call(this, callback, i, seqs);
253 });
254
255 function pusher (a) {
256 // Ridiculously faster than a.push.bind(a). :(
257 return function (o) { a.push(o); };
258 }
259
260 function map () {
261 /** Build an array by applying a function to sequences
262
263 Similar to Array.prototype.map, but allows passing multiple
264 sequences to call functions of any arity, as with each.
265
266 If you want to pass a thisArg to the callback, use
267 map.call(thisArg, callback, ...).
268 */
269 var a = [];
270 arguments[0] = compose(pusher(a), arguments[0]);
271 each.apply(this, arguments);
272 return a;
273 }
274
275 function mapr () {
276 /** Build an array by applying a function to sequences backwards
277
278 As eachr is to each, so mapr is to map.
279 */
280 var a = [];
281 arguments[0] = compose(pusher(a), arguments[0]);
282 eachr.apply(this, arguments);
283 return a;
284 }
285
286 function filter (callback, seq) {
287 /** Build an array without the elements that fail the predicate
288
289 Like Array.prototype.filter, but if null is passed for the
290 predicate, any false-y values will be removed.
291 */
292 var a = [];
293 callback = callback || I;
294 function _ (o) { if (callback.call(this, o)) a.push(o); }
295 each.call(this, _, seq);
296 return a;
297 }
298
299 function packed (f) {
300 /** Pack arguments to a function into an Array
301
302 packed(f)([a, b, c]) is equivalent to f(a, b, c).
303 */
304 return function (args) { return f.apply(this, args); };
305 }
306
307 function argcd () {
308 /** Create a function that dispatches based on argument count
309
310 Pass a list of functions with varying argument lengths, e.g.
311 var f = argcd(function () { ... },
312 function (a, b) { ... },
313 function (a, b, c) { ... });
314
315 This can be used for function overloading, clearer
316 defaults for positional parameters, or significantly more
317 evil things.
318
319 The functions can also be accessed directly by their
320 argument count, e.g. f[0], f[2], f[3] above.
321
322 If no matching argument count is found for a call, a
323 TypeError is raised.
324 */
325 if (arguments.length === 1)
326 throw new Error("don't use argcd for one function");
327 var table = (function table () {
328 return table[arguments.length].apply(this, arguments);
329 });
330 for (var i = 0; i < arguments.length; ++i)
331 table[arguments[i].length] = arguments[i];
332 return table;
333 }
334
335 var irange = argcd(
336 /** Call the provided callback counting as it goes
337
338 irange(x) counts from [0, x), by 1.
339
340 irange(x, y) counts from [x, y), by 1.
341
342 irange(x, y, s) counts from [x, y), by s.
343 */
344 function (callback, stop) {
345 return irange.call(this, callback, 0, stop, 1);
346 },
347 function (callback, start, stop) {
348 return irange.call(this, callback, start, stop, 1);
349 },
350 function (callback, start, stop, step) {
351 var length = Math.max(Math.ceil((stop - start) / step), 0);
352 for (var i = 0; i < length; ++i)
353 callback.call(this, start + step * i);
354 }
355 );
356
357 function capture1 (ifunc) {
358 /** Build an array from the values of an iterating function
359
360 The elements added to the array are the first arguments
361 the iterating function passes.
362 */
363 return function () {
364 var a = [];
365 var args = slice(arguments);
366 args.unshift(pusher(a));
367 ifunc.apply(this, args);
368 return a;
369 };
370 }
371
372 /** Generate an Array from range of numbers, as irange */
373 var range = capture1(irange);
374
375 function ipairs (callback, o) {
376 /** Call the provided callback with each key and value of the object */
377 each.call(this, function (k) { callback.call(this, k, o[k]); },
378 Object.keys(o));
379 }
380
381 function iprototypeChain (callback, o) {
382 /** Traverse an object and its prototype chain */
383 do {
384 callback.call(this, o);
385 } while ((o = Object.getPrototypeOf(o)));
386 }
387
388 function allKeys (o) {
389 /** Get *ALL* property names. Even unowned non-enumerable ones. */
390 var props = [];
391 iprototypeChain(compose(packed(props.push).bind(props),
392 Object.getOwnPropertyNames),
393 o);
394 return props.sort().filter(function (p, i, a) {
395 return p !== a[i - 1];
396 });
397 }
398
399 var some = argv(function (callback, seqs) {
400 callback = callback || I;
401 var length = Math.max.apply(Math, seqs.map(len));
402 var fn = calln(seqs.length);
403 for (var i = 0; i < length; ++i)
404 if (fn.call(this, callback, i, seqs))
405 return true;
406 return false;
407 });
408
409 var eachrUntil = argv(function (callback, seqs) { // Reverse of `some`
410 callback = callback || I;
411 var length = Math.max.apply(Math, seqs.map(len));
412 var fn = calln(seqs.length);
413 for (var i = length - 1; i >= 0; --i)
414 if (fn.call(this, callback, i, seqs))
415 return true;
416 return false;
417 });
418
419 function not (f) {
420 /** Build a function equivalent to !f(...) */
421 return function () { return !f.apply(this, arguments); };
422 }
423
424 function every () {
425 arguments[0] = not(arguments[0] || I);
426 return !some.apply(this, arguments);
427 }
428
429 function none () {
430 return !some.apply(this, arguments);
431 }
432
433 function repeat (o, n) {
434 return (new Array(n)).fill(o);
435 }
436
437 function arrayify (o) {
438 /** Equivalent to [].concat(o) but faster */
439 return Array.isArray(o) ? o : [o];
440 }
441
442 function stash (key, value, a) {
443 each(function (o) { o[key] = value; }, a);
444 }
445
446 function without (seq, o) {
447 if (arguments.length === 2)
448 return filter(function (a) { return a !== o; }, seq);
449 var os = slice(arguments, 1);
450 return filter(lacks.bind(null, os), seq);
451 }
452
453 function construct (ctr, args) {
454 args = slice(args);
455 args.unshift(ctr);
456 return new (Function.prototype.bind.apply(ctr, args))();
457 }
458
459 function new_ (ctr) {
460 return function () { return construct(ctr, arguments); };
461 }
462
463 function counter (start) {
464 var i = +(start || 0);
465 return function () { return i++; };
466 }
467
468 function volatile (f) {
469 return { valueOf: f };
470 }
471
472 function transform (callback, seq) {
473 irange.call(this, function (i) {
474 seq[i] = callback.call(this, seq[i]);
475 }, seq.length);
476 }
477
478 function mapValues (callback, o) {
479 var r = {};
480 for (var k in o)
481 r[k] = callback.call(this, o[k]);
482 return r;
483 }
484
485 function insertBefore (seq, o, before) {
486 var idx = seq.lastIndexOf(before);
487 if (idx === -1)
488 seq.push(o);
489 else
490 seq.splice(idx, 0, o);
491 return seq;
492 }
493
494 function seqEqual (a, b) {
495 if (a.length !== b.length)
496 return false;
497 for (var i = 0; i < a.length; ++i)
498 if (a[i] !== b[i])
499 return false;
500 return true;
501 }
502
503 function debounce (callback, wait) {
504 wait = wait || 100;
505 var this_ = null;
506 var args = null;
507 var id = null;
508
509 function _ () {
510 callback.apply(this_, args);
511 this_ = null;
512 args = null;
513 id = null;
514 }
515
516 return function () {
517 if (id !== null && this_ !== this)
518 _();
519 clearTimeout(id);
520 this_ = this;
521 args = arguments;
522 id = setTimeout(_, wait);
523 };
524 }
525
526 function pluck (prop, seq) {
527 return map(lookup(prop), seq);
528 }
529
530 Object.assign(this, {
531 allKeys: allKeys,
532 argcd: argcd,
533 argv: argv,
534 arrayify: arrayify,
535 clamp: clamp,
536 compose: compose,
537 construct: construct,
538 contains: contains,
539 counter: counter,
540 debounce: debounce,
541 each: each,
542 eachUntil: some,
543 eachr: eachr,
544 eachrUntil: eachrUntil,
545 every: every,
546 filter: filter,
547 foldl: foldl,
548 foldr: foldr,
549 getter: getter,
550 head: head,
551 I: I,
552 insertBefore: insertBefore,
553 ipairs: ipairs,
554 irange: irange,
555 isFunction: isFunction,
556 isString: isString,
557 K: K,
558 lacks: lacks,
559 last: last,
560 len: len,
561 lookup: lookup,
562 map: map,
563 mapr: mapr,
564 mapValues: mapValues,
565 max: max,
566 min: min,
567 most: most,
568 new_: new_,
569 none: none,
570 pluck: pluck,
571 range: range,
572 repeat: repeat,
573 seqEqual: seqEqual,
574 setter: setter,
575 slice: slice,
576 some: some,
577 stash: stash,
578 tail: tail,
579 transform: transform,
580 unbind: unbind,
581 volatile: volatile,
582 without: without,
583
584 VERSION: 0
585 });
586
587 }).call(typeof exports !== "undefined" ? exports : (this.yf = {}));