Skip to content

Extension functions for Arrays/Iterables that are compile-time converted to a single, optimal for-loop.

License

Notifications You must be signed in to change notification settings

SomeRanDev/Haxe-MagicArrayTools

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Magic Array Tools (Haxe)

Extension functions for Arrays/Iterables that are compile-time converted to a single, optimal for-loop. Never again be concerned about performance when you need to throw on a couple maps and filters. Any number of array modifications is guarenteed to run through just one loop at runtime!

// Place at top of file or in import.hx
using MagicArrayTools;

// ---

var arr = ["a", "i", "the", "and"];

// At compile-time this code is
// converted into a single for-loop.
arr.filter(s -> s.length == 1)
   .map(s -> s.charCodeAt(0))
   .filter(s -> s != null)
   .count(s -> s == 105);

//    |
//    V

// This is what is generated and replaces 
// the expression at compile-time.
{
    var result = 0;
    for(it in arr) {
        if(it.length != 1) continue;
        final it2 = it.charCodeAt(0);
        if(it2 == null) continue;
        if(it2 == 105) {
            result++;
        }
    }
    result;
}

[Installation]

# What to do What to write
1 Install via haxelib.
haxelib install magic-array-tools
2 Add the lib to your .hxml file or compile command.
-lib magic-array-tools
3 Add this top of your source file or import.hx.
using MagicArrayTools;

Now use this library's functions on an Array, Iterable, or Iterator and let the magic happen!


[Feature Index]

Feature Description
Inline Mode A shorter, faster syntax for callbacks
Disable Auto For-Loop Temporarily or permanently disable the automatic for-loop creation
Display For-Loop Stringifies and traces the for-loop code that will be generated for debugging purposes
map and filter Remapping and filtering functions
forEach and forEachThen Iterate and run an expression or callback
size and isEmpty Finds the number of elements
count Counts the number of elements that match the condition
find and findIndex Finds the first element that matches the condition
indexOf Returns the index of the provided element
every and some Check if some or all elements match the condition
reduce Reduce to single value summed together using function
asList and asVector Provides the result as a haxe.ds.List or haxe.ds.Vector
concat Appends another Array, Iterable, or even separate for-loop
fill Fill a subsection or the entire Array with a value

[Features]

Inline Mode

While local functions can be passed as an argument, for short/one-line operations it is recommended "inline mode" is used. This resolves any issues that comes from Haxe type inferences, and it helps apply the exact expression where desired.

Any function that takes a callback as an argument can accept an expression (Expr) that's just the callback body. Use a single underscore identifier (_) to represent the argument that would normally be passed to the callback (usually the processed array item). This expression will be placed and typed directly in the resuling for-loop.

[1, 2, 3].map(i -> "" + i); // Error:
                            // Int should be String
                            // ... For function argument 'i'

[1, 2, 3].map("" + _);            // Fix using inline mode!
[1, 2, 3].map((i:Int) -> "" + i); // (Explicit-typing also works)

 

Disable Auto For-Loop

In certain circumstances, one may want to disable the automatic for-loop building. Using the @disableAutoForLoop metadata will disable this for all subexpressions. However, for-loops can still be manually constructed by appending .buildForLoop().

If manually building for-loops using buildForLoop() is preferred, defining the compilation flag (-D disableAutoForLoop) will disable the automatic building for the entire project.

class ConflictTester {
    public function new() {}
    public function map(c: (Int) -> Int) return 1234;
    public function iterator(): Iterator<Int> { return 0...5; }
}

// ---

final obj = new ConflictTester();

// This generates a for-loop.
obj.map(i -> i);

// Unable to call "map" function on this
// Iterable unless auto for-loops are disabled.
@disableAutoForLoop {
    obj.map(i -> i);                // 1234
    obj.map(i -> i).buildForLoop(); // [0, 1, 2, 3, 4]
}

 

Display For-Loop

Curious about the code that will be generated? Simply append .displayForLoop() to the method chain, and the generated for-loop expression will be traced/printed to the console.

["a", "b", "c"]
    .map(_.indexOf("b"))
    .filter(_ >= 0)
    .asList()
    .displayForLoop();

// -- OUTPUT --
//
// Main.hx:1: {
//     final result = new haxe.ds.List();
//     for(it in ["a", "b", "c"]) {
//         final it2 = it.indexOf("b");
//         if(!(it2 >= 0)) continue;
//         result.add(it2);
//     }
//     result;
// }
// 

 

map and filter

These functions work exactly like the Array's map and filter functions.

function map(callback: (T) -> U): Array<U>;
function filter(callback: (T) -> Bool): Array<T>;
var arr = [1, 2, 3, 4, 5];

var len = arr.filter(_ < 2).length;
assert(len == 1);


var spaces = arr.map(StringTools.lpad("", " ", _));
assert(spaces[2] == "   ");

 

forEach and forEachThen

Calls the provided function/expression on each element in the Array/Iterable. forEachThen will return the array without modifying the elements. On the other hand, forEach returns Void and should be used in cases where the iterable object is not needed afterwards.

function forEach(callback: (T) -> Void): Void;
function forEachThen(callback: (T) -> Void): Array<T>;
// do something 10 times
(0...10).forEach(test);

//    |
//    V

for(it in 0...10) {
    test(it);
}
// add arbitrary behavior within for-loop
["i", "a", "bug", "hello"]
    .filter(_.length == 1)
    .forEachThen(trace("Letter is: " + _))
    .map(_.charCodeAt(0));

//    |
//    V

{
    final result = [];
    for(it in ["i", "a", "bug", "hello"]) {
        if(it.length != 1) continue;
        trace("Letter is: " + it);
        final it2 = it.charCodeAt(0);
                                                                    
        result.push(it2);
    }
    result;
}

 

size and isEmpty

size counts the number of elements after the other modifiers are applied. isEmpty is an optimized version that immediately returns false upon the first element found and returns true otherwise.

function size(): Int;
function isEmpty(): Bool;
(0...5).filter(_ % 2 == 0).size();

//    |
//    V

{
    var result = 0;
    for(it in 0...5) {
        if(it % 2 != 0) continue;
        result++;
    }
    result;
}
(10...20).filter(_ == 0).isEmpty();

//    |
//    V

{
    var result = true;
    for(it in 10...20) {
        if(it != 0) continue;
        result = false;
        break;
    }
    result;
}

 

count

count counts the number of elements that match the condition.

function count(callback: (T) -> Bool): Int;
(0...20).count(_ > 10);

//    |
//    V

{
    var result = 0;
    for(it in 0...20) {
        if(it > 10) {
            result++;
        }
    }
    result;
}

 

find and findIndex

find returns the first element that matches the condition. findIndex does the same thing, but it returns the index of the element instead.

function find(callback: (T) -> Bool): Null<T>;
function findIndex(callback: (T) -> Bool): Int;
["ab", "a", "b", "cd"].find(_.length <= 1);

//    |
//    V

{
    var result = null;
    for(it in ["ab", "a", "b", "cd"]) {
        if(it.length <= 1) {
            result = it;
            break;
        }
    }
    result;
}
vectorIterator.findIndex(_.magnitude > 3);

//    |
//    V

{
    var result = -1;
    var i = 0;
    for(it in vectorIterator) {
        if(it.magnitude > 3) {
            result = i;
            break;
        }
        i++;
    }
    result;
}

 

indexOf

indexOf returns the index of the first element that equals the provided argument. This function has three arguments, but only the first one is required.

function indexOf(item: T, startIndex: Int = 0, inlineItemExpr: Bool = false): Int;

startIndex dictates the number of elements that must be processed before initiating the search. This functionality will not be generated at all as long as the argument is not provided or the argument is assigned a 0 literal.

inlineItemExpr is a compile-time argument that must either a true or false literal. It defines how the item expression will be used in the generated for-loop. If true, the expression will be inserted into the for-loop exactly as passed. If not provided or false, the expression will be assigned to a variable, and this variable will be used within the for-loop. Single identifiers and numbers will be automatically inlined since there is no additional runtime cost.

[22, 33, 44].indexOf(33);

//    |
//    V

{
    var result = -1;
    var i = 0;
    for(it in [22, 33, 44]) {
        if(it == 33) {
            result = i;
            break;
        }
        i++;
    }
    result;
}
// If the third argument was "true", the "_value" variable would not be generated.
// Instead, the comparison would be: if(it == World.FindPlayer())
// FindPlayer might be an expensive operation, so this is not the default behavior.
EntitiesIterator.indexOf(World.FindPlayer(), 1, false);

//    |
//    V

{
    var result = -1;
    var i = 0;
    final _value = World.FindPlayer();
    var _indexOfCount: Int = 1;
    for(it in EntitiesIterator) {
        if(_indexOfCount > 0) {
            _indexOfCount--;
        } else if(it == _value) {
            result = i;
            break;
        }
        i++;
    }
    result;
}

 

every and some

every returns true if every element returns true when passed to the provided callback. On the other hand, some returns true as long as at least one element passes.

function every(callback: (T) -> Bool): Bool;
function some(callback: (T) -> Bool): Bool;
[75, 7, 12, 93].every(_ > 0);

//    |
//    V

{
    var result = true;
    for(it in [75, 7, 12, 93]) {
        if(it <= 0) {
            result = false;
            break;
        }
    }
    result;
}
(1...10).some(_ == 4);

//    |
//    V

{
    var result = false;
    for(it in 1...10) {
        if(it == 4) {
            result = true;
            break;
        }
    }
    result;
}

 

reduce

reduce calls a function on every element to accumulate all the values. The returned value of the previous call is passed as the first argument; the second argument is the element being iterated on. The returned value of the final call is what reduce returns.

function reduce(callback: (T, T) -> T): T;
["a", "b", "c", "d"].reduce((a, b) -> a + b);

//    |
//    V

{
    var result = null;
    var _hasFoundValue = false;
    for(it in ["a", "b", "c", "d"]) {
        if(!_hasFoundValue) {
            _hasFoundValue = true;
            result = it;
        } else {
            result = result + it;
        };
    };
    result;
}

 

asList and asVector

These functions change the resulting data-structure to either be a haxe.ds.List or haxe.ds.Vector.

function asList(): haxe.ds.List<T>;
function asVector(): haxe.ds.Vector<T>;
(0...10).filter(_ % 3 != 0).asList();

//    |
//    V

{
    var result = new haxe.ds.List();
    for(it in 0...10) {
        if(it % 3 == 0) continue;
        result.add(it);
    }
    result;
}
(0...10).filter(_ % 3 != 0).asVector();

//    |
//    V

{
    var result = [];
    for(it in 0...10) {
        if(it % 3 == 0) continue;
        result.push(it);
    }
    haxe.ds.Vector.fromArrayCopy(result);
}

 

concat

Appends the provided array/elements to the current array. The output generates an additional for-loop to iterate over the new elements. This library's functions can be called on the first argument to this function, and the modifiers will be recursively flattened and applied exclusively to the new loop.

function concat(other: Array<T> | Iterable<T> | Iterator<T>): Array<T>;
(0...10).concat([100, 1000, 9999]);

//    |
//    V

{
    var result = [];
    for(it in 0...10) {
        result.push(it);
    }
    for(it in [100, 1000, 9999]) {
        result.push(it);
    }
    result;
}
// Pass a "for-loop" as an argument and it will be merged.
(0...10).filter(_ % 2 == 0).concat( (0...10).filter(_ % 3 == 0) );

//    |
//    V

{
    var result = [];
    for(it in 0...10) {
        if(it % 2 != 0) continue;
        result.push(it);
    }
    for(it in 0...10) {
        if(it % 3 != 0) continue;
        result.push(it);
    }
    result;
}

 

fill

fill fills the resulting Array with the provided value. A subsection can be filled using the second and third arguments.

function fill(value: T, startIndex: Int = 0, endIndex: Int = this.length): Array<T>;
[1, 2, 3].fill(10);

//    |
//    V

{
    var result = [];
    for(it in [1, 2, 3]) {
        var it2 = 10;
        result.push(it2);
    };
    result;
}
(0...10).fill(999, 2, 8);

//    |
//    V

{
    var result = [];
    var i = 0;
    for(it in (0 ... 10)) {
        var it2 = if((i >= 2) && (i < 8)) {
            999;
        } else {
            it;
        };
        result.push(it2);
        i++;
    };
    result;
}

 

About

Extension functions for Arrays/Iterables that are compile-time converted to a single, optimal for-loop.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages