Last active
June 13, 2016 23:38
-
-
Save inmatarian/5ea8da95491b25c8c362 to your computer and use it in GitHub Desktop.
latest version of funky.lua
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- Funky.lua, collection of functional tools | |
local _ENV = setmetatable({}, {__index = _ENV or _G}) | |
if setfenv then setfenv(1, _ENV) end | |
unpack = unpack or table.unpack | |
local __ = {} | |
do | |
-- We need a fast way to check for placeholders. All funkier libraries | |
-- will need to register themselves as a funky placeholder. | |
local funky_cache = setmetatable({}, {__mode='k'}) | |
function __.IS_FUNKY(obj) | |
return funky_cache[obj] | |
end | |
IS_FUNKY = __.IS_FUNKY | |
function __.ADD_FUNKY(obj) | |
funky_cache[obj] = true | |
end | |
__.ADD_FUNKY(__) | |
end | |
-- Partial Application and Currying are used internally -- | |
--- Calls function with an array converted into arguments | |
function __.apply(fn, array) | |
return fn(unpack(array)) | |
end | |
apply = __.apply | |
--- One based index of a value in an array, or 0 if value wasn't found | |
function __.indexOf(value, array) | |
for i = 1, #array do | |
if array[i] == value then | |
return i | |
end | |
end | |
return 0 | |
end | |
indexOf = __.indexOf | |
function append_or_replace_placeholders(binding, ...) | |
local results, p = {}, 1 | |
for i = 1, #binding do | |
if IS_FUNKY(binding[i]) then | |
results[i] = select(p, ...) | |
p = p + 1 | |
else | |
results[i] = binding[i] | |
end | |
end | |
for i = p, select('#', ...) do | |
results[#results+1] = select(i, ...) | |
end | |
return results | |
end | |
--- Partial Application of a function using __ as a place-holder | |
-- Special case optimizations for 1 and 2-arity partials | |
-- @usage fn2 = __.partial(fn1, some, parameters); fn2(some, more) | |
-- @usage obj.method2 = __.partial(obj.method1, __, some, params); obj:method2() | |
function __.partial(fn, ...) | |
local N, arg1, arg2 = select('#', ...), select(1, ...) | |
if N == 1 and arg1 ~= __ then | |
return function(...) return fn(arg1, ...) end | |
elseif N == 2 and arg1 ~= __ and arg2 ~= __ then | |
return function(...) return fn(arg1, arg2, ...) end | |
else | |
local binding = {...} | |
return function(...) | |
return apply(fn, append_or_replace_placeholders(binding, ...)) | |
end | |
end | |
end | |
partial = __.partial | |
function curry_helper(fn, args, trace) | |
if indexOf(__, trace) > 0 or #trace < args then | |
return function(...) | |
return curry_helper(fn, args, append_or_replace_placeholders(trace, ...)) | |
end | |
else | |
return apply(fn, trace) | |
end | |
end | |
--- Curries a function, each call returns another function with an additional param supplied | |
-- Different from partial application, go learn you some computer science to understand why | |
-- Use __ as a placeholder for right-currying | |
-- @param fn Function to curry | |
-- @param args Number of parameters needed before final call happens | |
-- @param ... initial parameters to supply to save time. | |
-- @return curried function (or final call if all params supplied at start) | |
function __.curry(fn, args, ...) | |
return curry_helper(fn, args, {...}) | |
end | |
-- these aren't alphabetically sorted, use with __.compare | |
__.op = {} | |
function __.op.eq(a, b) return a == b end | |
function __.op.neq(a, b) return a ~= b end | |
function __.op.lt(a, b) return a < b end | |
function __.op.gt(a, b) return a > b end | |
function __.op.lte(a, b) return a <= b end | |
function __.op.gte(a, b) return a >= b end | |
function __.op.add(a, b) return a + b end | |
function __.op.sub(a, b) return a - b end | |
function __.op.mult(a, b) return a * b end | |
function __.op.div(a, b) return a / b end | |
function __.op.exp(a, b) return a ^ b end | |
function __.op.mod(a, b) return a % b end | |
function __.op.neg(a) return -a end | |
function __.op.concat(a, b) return a .. b end | |
function __.op.land(a, b) return a and b end | |
function __.op.lor(a, b) return a or b end | |
function __.op.lnot(a) return not a end | |
function __.op.len(a) return #a end | |
-- op_eq, op_neq, etc added to this module's environment | |
for k, v in pairs(__.op) do | |
_ENV['op_'..k]=v | |
end | |
--- Checks for all values return truthy from the predicate | |
-- @param predicate function use for checking values in the form (value, ...) -> boolean | |
-- @param array values checked with the predicate function | |
-- @param ... optional values sent to the predicate function | |
-- @return true if all pass, false otherwise, with the index and the bad value | |
function __.all(predicate, array, ...) | |
for i = 1, #array do | |
if not predicate(array[i], ...) then | |
return false | |
end | |
end | |
return true, 0, nil | |
end | |
--- Checks for any values return truthy from the predicate | |
-- @param predicate function use for checking values in the form (value, ...) -> boolean | |
-- @param array values checked with the predicate function | |
-- @param ... optional values sent to the predicate function | |
-- @return true if found, and the index and that value, false otherwise | |
function __.any(predicate, array, ...) | |
for i = 1, #array do | |
if predicate(array[i], ...) then | |
return true, i, array[i] | |
end | |
end | |
return false, 0, nil | |
end | |
--- Create a shallow copy of an object (and assigns the same metatable) | |
-- @param object source object to be cloned | |
-- @return the new object following cloning | |
function __.clone(object) | |
local copy = {} | |
for k, v in pairs(object) do | |
copy[k] = v | |
end | |
return setmetatable(copy, getmetatable(object)) | |
end | |
function vararg_compare(cmp, ...) | |
local N = select('#', ...) - 1 | |
for i = 1, N do | |
if not cmp(select(i, ...)) then return false end | |
end | |
return true | |
end | |
--- Compare all elements across arrays to see if they pass criteria | |
function __.compare(comparer, ...) | |
local rows = {...} | |
local N, M = #rows, #rows[1] | |
for r = 2, N do | |
if #(rows[r]) ~= M then return false end | |
end | |
local column = {} | |
for i = 1, M do | |
for r = 1, N do | |
column[r] = rows[r][i] | |
end | |
if not vararg_compare(comparer, unpack(column)) then return false end | |
end | |
return true | |
end | |
function compose_apply(fns, idx, ...) | |
if idx > 1 then | |
return compose_apply(fns, idx-1, fns[idx](...)) | |
else | |
return fns[1](...) | |
end | |
end | |
--- Compose functions together in right-to-left pipeline | |
-- @example compose(f, g, h)(...) == f(g(h(...))) | |
function __.compose(...) | |
local fns = {...} | |
return function(...) | |
return compose_apply(fns, #fns, ...) | |
end | |
end | |
--- Connect functions together in a left-to-right pipeline | |
-- @example pipe(f, g, h)(...) == h(g(f(...))) | |
function __.pipe(...) | |
local fns = __.reverse({...}) | |
return function(...) | |
return compose_apply(fns, #fns, ...) | |
end | |
end | |
--- Combines arrays into one, not taking into account duplicates | |
function __.concat(...) | |
local results = {} | |
for i = 1, select('#', ...) do | |
local array = select(i, ...) | |
for j = 1, #array do | |
results[#results+1] = array[j] | |
end | |
end | |
return results | |
end | |
--- Specialized form of reduce ([string] -> string) with delimiter merging | |
function __.concatstr(delimiter, array) | |
if type(delimiter) == "string" or type(delimiter)=="number" then | |
return table.concat(array, tostring(delimiter)) | |
elseif array == nil then | |
return table.concat(delimiter) | |
end | |
if #array < 1 then return '' end | |
local result = array[1] | |
for i = 2, #array do | |
result = delimiter(result, array[i]) | |
end | |
return result | |
end | |
--- Returns the same values over successive calls | |
function __.constant(...) | |
local v = {...} | |
return function() return unpack(v) end | |
end | |
--- Count the number of elements in an array that passes criteria | |
function __.count(predicate, array) | |
local cnt = 0 | |
for i = 1, #array do | |
if predicate(array[i], i)==true then | |
cnt = cnt + 1 | |
end | |
end | |
return cnt | |
end | |
--- Creates a deep clone of an object (self references converted) | |
-- @param object source object to be cloned | |
-- @return the new object following cloning | |
function __.deepclone(object, copy, cycle) | |
copy = copy or {} | |
cycle = cycle or {} | |
cycle[object] = copy | |
for k, v in pairs(object) do | |
if type(v) == "table" then | |
if cycle[v] then | |
copy[k] = cycle[v] | |
else | |
copy[k] = __.deepclone(v, cycle[v], cycle) | |
end | |
else | |
copy[k] = v | |
end | |
end | |
if getmetatable(copy) == nil then setmetatable(copy, getmetatable(object)) end | |
return copy | |
end | |
--- Iterates through an array calling on each value | |
-- return __ to stop the iteration | |
-- @param fn call to make on each element (value, key, array, ...) -> any | |
-- @param array the list to walk each element | |
function __.each(fn, array, ...) | |
for i = 1, #array do | |
local ret = fn(array[i], i, array, ...) | |
if IS_FUNKY(ret) then break end | |
end | |
end | |
--- Iterates through an array in reverse order calling on each value | |
-- return __ to stop the iteration | |
-- @param fn call to make on each element (value, key, array, ...) -> any | |
-- @param array the list to walk each element | |
function __.eachRight(fn, array, ...) | |
for i = #array, 1, -1 do | |
local ret = fn(array[i], i, array, ...) | |
if IS_FUNKY(ret) then break end | |
end | |
end | |
--- If the string haystack ends with needle | |
function __.endsWith(needle, haystack) | |
return string.find(haystack, needle, -#needle, true) ~= nil | |
end | |
--- Copies all owner properties from the others to object | |
-- object is modified by this action, breaks referential transparency | |
-- @param object the object to be modified | |
-- @param other the object to copy properties from | |
-- @param ... more objects to copy from | |
-- @returns the modified object | |
function __.extend(object, other, ...) | |
for k, v in pairs(other) do | |
object[k] = v | |
end | |
if select('#', ...) == 0 then | |
return object | |
else | |
return extend(object, ...) | |
end | |
end | |
extend = __.extend | |
--- Collects the elements of an array that pass as truthy from the predicate | |
-- @param predicate function to test elements (value, index, array)->boolean | |
-- @param array the array to collect from | |
-- @return a new list containing the desired elements | |
function __.filter(predicate, array) | |
local result = {} | |
for i = 1, #array do | |
if predicate(array[i], i, array) then | |
result[#result + 1] = array[i] | |
end | |
end | |
return result | |
end | |
--- Flattens nested arrays into a single array | |
-- @usage flatten({1, 2, {3, 4, {5, 6}}, 7}) -> {1, 2, 3, 4, {5, 6}, 7} | |
-- @usage flatten({1, 2, {3, 4, {5, 6}}, 7}, true) -> {1, 2, 3, 4, 5, 6, 7} | |
-- @param array the list to be flattened | |
-- @param deep truthy if this should be a recursive flatten | |
-- @return a new array | |
function __.flatten(array, deep) | |
local result = {} | |
for i = 1, #array do | |
if type(result) == "table" then | |
local cell | |
if deep then cell = __.flatten(array[i], deep) else cell = array[i] end | |
for j = 1, #cell do | |
result[#result+1] = cell[j] | |
end | |
else | |
result[#result+1] = array[i] | |
end | |
end | |
return result | |
end | |
--- Turns a generator into a list | |
-- For heaven sake don't try this: __.from(__.range()) | |
function __.from(generator) | |
local results, value = {}, generator() | |
while value ~= nil do | |
results[#results] = value | |
value = generator() | |
end | |
return results | |
end | |
--- Collects the first N elements of an array | |
-- @param array the array to collect from | |
-- @param n optional number of elements to collect (default is 1) | |
-- @return a new list containing the desired elements | |
function __.head(n, array) | |
if array == nil then | |
array = n | |
n = 1 | |
end | |
local result = {} | |
for i = 1, n do | |
result[i] = array[i] | |
end | |
return result | |
end | |
--- Identity function returns all parameters unchanged | |
function __.identity(...) return ... end | |
identity = __.identity | |
--- Collects unique values that appear in all arrays | |
function __.intersect(array, ...) | |
local results = {} | |
for i = 1, #array do | |
local value, inAll = array[i], true | |
for j = 1, select('#', ...) do | |
local found, other = false, select(j, ...) | |
for k = 1, #other do | |
if other[k] == value then found = true; break end | |
end | |
if not found then inAll = false; break end | |
end | |
if inAll then | |
for r = 1, #results do | |
if results[r] == value then value = nil; break end | |
end | |
if value ~= nil then | |
results[#results+1] = value | |
end | |
end | |
end | |
return results | |
end | |
-- is functions depend on __.partial, placed out of alphabetical order | |
function __.is(typename, ...) | |
for i = 1, select('#', ...) do | |
if type((select(i, ...))) ~= typename then return false end | |
end | |
return true | |
end | |
__.isNil = partial(__.is, "nil") | |
__.isNumber = partial(__.is, "number") | |
__.isString = partial(__.is, "string") | |
__.isBoolean = partial(__.is, "boolean") | |
__.isTable = partial(__.is, "table") | |
__.isFunction = partial(__.is, "function") | |
__.isThread = partial(__.is, "thread") | |
__.isUserdata = partial(__.is, "userdata") | |
--- Returns all keys from an object | |
function __.keys(object) | |
local result = {} | |
for k, _ in pairs(object) do result[#result+1] = k end | |
return result | |
end | |
do | |
local lambda_cache = setmetatable({}, { __mode = 'v' }) | |
local lambda_env = setmetatable({ __=__ }, { __index = getmetatable(_ENV).__index }) | |
-- 5.2 compatible | |
local loadstring = loadstring or function(str, name) | |
return load(str, name, 't', lambda_env) | |
end | |
--- Lambda Function definitions | |
-- Short anonymous functions, using any run of : - = > as the separator | |
-- Doesn't have a statement/expression separator. | |
-- @param an expression string in the form "params => expression" | |
-- @return an anonymous function | |
function __.lambda(expression) | |
local fn = lambda_cache[expression] | |
if not fn then | |
local args, body = string.match(expression, "([^:%-=>]+)[:%-=>]+(.*)") | |
local definition = "return function(" .. args .. ") return ".. body .." end" | |
local chnk, err = loadstring(definition, expression) | |
if not chnk then error(err) end | |
fn = chnk() | |
if setfenv then setfenv(fn, lambda_env) end | |
lambda_cache[expression] = fn | |
end | |
return fn | |
end | |
end | |
--- Collects the new elements of an array after passed through a morphism | |
-- @param morph function to change elements (value, index, array)->any | |
-- @param array the array to collect from | |
-- @return a new list containing the desired elements | |
function __.map(morph, array) | |
local result = {} | |
for i = 1, #array do | |
result[i] = morph(array[i], i, array) | |
end | |
return result | |
end | |
function maybe_decorator(fn, ...) | |
local value = ... | |
if value ~= nil then | |
return fn(...) | |
else | |
return nil | |
end | |
end | |
--- Special function decorator, checks first parameter for nil to short circuit | |
function __.maybe(...) | |
local N = select('#', ...) | |
if N == 1 then | |
return partial(maybe_decorator, ...) | |
else | |
local result = {} | |
for i = 1, N do | |
result[i] = partial(maybe_decorator, (select(i, ...))) | |
end | |
return unpack(result) | |
end | |
end | |
-- Not alphabetized here because of related functionality | |
function find_best(value, cmp, fn, array) | |
local best, best_id = nil, nil | |
for i = 1, #array do | |
local test = fn(array[i]) | |
if cmp(test, value) then | |
value = test | |
best = array[i] | |
best_id = i | |
end | |
end | |
return best, best_id | |
end | |
function make_pluck(prop) | |
return function(obj) return obj[prop] end | |
end | |
--- Maximum | |
function __.max(accessor, array) | |
if type(accessor)=="string" or type(accessor)=="number" then | |
accessor = make_pluck(accessor) | |
end | |
return find_best(-math.huge, op_gte, accessor, array) | |
end | |
--- Minimum | |
function __.min(accessor, array) | |
if type(accessor)=="string" or type(accessor)=="number" then | |
accessor = make_pluck(accessor) | |
end | |
return find_best(math.huge, op_lte, accessor, array) | |
end | |
--- Extents, both Min and Max at the same time | |
function __.extents(accessor, array, ...) | |
if type(accessor)=="string" or type(accessor)=="number" then | |
accessor = make_pluck(accessor) | |
end | |
local best_min, best_max, best_min_id, best_max_id | |
local min_value, max_value = math.huge, -math.huge | |
for i = 1, #array do | |
local test = accessor(array[i], ...) | |
if test <= min_value then | |
min_value = test | |
best_min_id = i | |
best_min = array[i] | |
end | |
if test >= max_value then | |
max_value = test | |
best_max_id = i | |
best_max = array[i] | |
end | |
end | |
return best_min, best_max, best_min_id, best_max_id | |
end | |
--- Locate index of an item based off of a property accessor | |
function __.find(accessor, array, ...) | |
for i = 1, #array do | |
if accessor(array[i], ...) then return i end | |
end | |
return 0 | |
end | |
--- Locate index of an item based off of a property accessor | |
function __.findRight(accessor, array, ...) | |
for i = #array, 1, -1 do | |
if accessor(array[i], ...) then return i end | |
end | |
return 0 | |
end | |
--- Memoize | |
function __.memoize(fn, hash) | |
hash = hash or identity | |
local cache = {} | |
return function(...) | |
local key = hash(...) | |
if cache[key] == nil then | |
cache[key] = {fn(...)} | |
end | |
return unpack(cache[key]) | |
end | |
end | |
--- Function that returns nothing | |
function __.null() end | |
--- Specialized version of memoize that hashes all values to a single key | |
function __.once(fn) | |
local cache | |
return function(...) | |
if cache == nil then | |
cache = {fn(...)} | |
fn = nil -- allow gc | |
end | |
return unpack(cache) | |
end | |
end | |
--- Formats a table into a string | |
-- If the table only contains strings, numbers, and booleans, then it | |
-- should be safe to use as a serialization function | |
function __.pretty(obj, sp, cycle) | |
cycle = cycle or {} | |
if type(obj)=="string" then | |
return obj | |
elseif type(obj)~="table" then | |
return tostring(obj) | |
else | |
cycle[obj] = true | |
sp = sp or "" | |
local s = { "{" } | |
for k, v in pairs(obj) do | |
local indent = sp .. " " | |
local name | |
if type(k)=="string" then name = k else name = ("[" .. tostring(k) .. "]") end | |
local val = cycle[v] and ("[cyclic reference " .. tostring(v) .. "]") or v | |
s[#s+1] = indent .. name .. " = " .. __.pretty(val, indent, cycle) .. "," | |
end | |
s[#s+1]= sp.."}" | |
return table.concat(s, '\n') | |
end | |
end | |
--- Number generator | |
-- @param start optional | |
-- @param stop inclusive manimum, count endlessly if omitted | |
-- @param step optional | |
-- @return function | |
function __.range(start, stop, step) | |
if (start == nil) and (stop == nil) then | |
start, step = 1, step or 1 | |
return function() | |
local x = start | |
start = start + step | |
return x | |
end | |
else | |
if (stop == nil) then start, stop = 1, start end | |
if (step == nil) then step = stop >= start and 1 or -1 end | |
local cmp = step > 0 and op_gt or op_lt | |
return function() | |
if cmp(start, stop) then return nil end | |
local x = start | |
start = start + step | |
return x | |
end | |
end | |
end | |
--- Reduce an array down to a single value | |
-- | |
function __.reduce(fn, array, initial) | |
local result = initial or 0 | |
for i = 1, #array do | |
result = fn(result, array[i], i) | |
end | |
return result | |
end | |
--- Collects the rest an array after the head | |
-- @param array the array to collect from | |
-- @param idx optional starting element to collect from | |
-- @param last optional last element to collect from | |
-- @return a new list containing the desired elements | |
function __.rest(idx, last, array) | |
if last == nil then | |
array = idx | |
idx = 2 | |
last = #array | |
elseif array == nil then | |
array = last | |
last = #array | |
end | |
local result = {} | |
for i = idx, last do | |
result[#result + 1] = array[i] | |
end | |
return result | |
end | |
--- Reverse an array | |
-- @return a new array | |
function __.reverse(array) | |
local result = {} | |
for i = #array, 1, -1 do | |
result[#result+1] = array[i] | |
end | |
return result | |
end | |
--- Picks a random number of elements from an array | |
function __.sample(rnd, num, array) | |
local results = __.shuffle(rnd, array) | |
for i = #results, num+1, -1 do results[i] = nil end | |
return results | |
end | |
--- Fisher-yates shuffle | |
function __.shuffle(rnd, array) | |
local result = {} | |
for i = 1, #array do | |
local j = rnd(1, i) | |
if j == i then | |
result[#result+1] = array[i] | |
else | |
result[#result+1] = result[j] | |
result[j] = array[i] | |
end | |
end | |
return result | |
end | |
--- Return a sorted copy of an array | |
-- @param comparator function(a, b) return a < b end | |
function __.sorted(comparator, array) | |
local results = {} | |
for i = 1, #array do results[i] = array[i] end | |
table.sort(results, comparator) | |
return results | |
end | |
function sanitize_pattern_character (c) return '%'..c end | |
--- Split a string based on a seperator character | |
function __.split(sep, str) | |
if str == nil and type(sep)=="string" then | |
str, sep = sep, " " | |
elseif type(delimiter) == "table" then | |
sep = table.concat(sep) | |
end | |
sep = string.gsub(sep, "[%^%$%(%)%%%.%[%]%*%+%-%?]", sanitize_pattern_character) | |
local result, start, match, word = {}, 0, 0 | |
repeat | |
match = string.find(str, sep, start+1) or 0 | |
if match <= start then | |
word = string.sub(str, start+1) | |
else | |
word = string.sub(str, start+1, match-1) | |
start = match | |
end | |
result[#result+1] = word | |
until match < 1 | |
return result | |
end | |
--- If the string haystack starts with needle | |
function __.startsWith(needle, haystack) | |
return string.find(haystack, needle, 1, true) == 1 | |
end | |
--- Collects the last N elements of an array | |
-- @param array the array to collect from | |
-- @param n optional number of elements to collect (default is 1) | |
-- @return a new list containing the desired elements | |
function __.tail(n, array) | |
if array == nil then | |
array = n | |
n = 1 | |
end | |
local result = {} | |
local k = #array - n + 1 | |
for i = k, #array do | |
result[#result+1] = array[i] | |
end | |
return result | |
end | |
--- Map or Reduce all of the elements of a table to another | |
-- | |
function __.transform(fn, object, initial) | |
local result = initial or {} | |
for k, v in pairs(object) do | |
local res = fn(result, k, v) | |
if res ~= nil then resukt = res end | |
end | |
return result | |
end | |
--- Remove whitespace from left and right of a string | |
function __.trim(str) | |
local from = string.match(str, "^%s*()") | |
return from > #str and "" or string.match(str, ".*%S", from) | |
end | |
--- Takes the union of arrays, removing duplicates | |
function __.union(...) | |
local results = {} | |
for i = 1, select('#', ...) do | |
local array = select(i, ...) | |
for j = 1, #array do | |
local value = array[j] | |
for k = 1, #results do | |
if results[k] == value then value = nil; break end | |
end | |
if value ~= nil then results[#results+1] = value end | |
end | |
end | |
return results | |
end | |
--- Remove duplicate items from the array | |
function __.unique(cmp, array) | |
local results = {} | |
for i = 1, #array do | |
local found = false | |
for j = 1, #results do | |
found = found or cmp(array[i], results[j]) | |
if found then break end | |
end | |
if not found then results[#results+1] = array[i] end | |
end | |
return results | |
end | |
--- Returns all values from an object | |
function __.values(object) | |
local result = {} | |
for _, v in pairs(object) do result[#result+1] = v end | |
return result | |
end | |
--- Combines multiple arrays into an array of tuples from each | |
-- Arrays of unequal lengths will NOT nil pad the resulting tuples | |
-- @usage zip({1, 3, 5}, {2, 4, 6}) -> {{1, 2}, {3, 4}, {5, 6}} | |
-- @usage zip({1}, {2, 4}, {3}) -> {{1, 2, 3}, {4}} | |
-- @param ... the arrays to copy elements from | |
-- @return a new array | |
function __.zip(...) | |
local max = 0 | |
local result = {} | |
for k = 1, select('#', ...) do | |
local array = select(k, ...); | |
for i = 1, #array do | |
result[i] = result[i] or {} | |
result[i][#result[i]+1] = array[i] | |
end | |
end | |
return result | |
end | |
-------------------------------------------------------------------------------- | |
if __TESTING then loadfile("funky/funky_tests.lua")(__) end | |
-------------------------------------------------------------------------------- | |
return __ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local _ENV = setmetatable({}, {__index = _ENV or _G}) | |
if setfenv then setfenv(1, _ENV) end | |
__ = ... | |
if __ == nil then | |
full_messages = true | |
local locations = {"../funky.lua", "funky.lua"} | |
local messages = {} | |
local funky_chunk | |
for i, v in ipairs(locations) do | |
funky_chunk, messages[i] = loadfile(v) | |
if funky_chunk then __ = funky_chunk() break end | |
end | |
if not funky_chunk then | |
for i, v in ipairs(messages) do | |
print(i, v) | |
end | |
print() | |
end | |
end | |
assert(__ ~= nil) | |
it = {} | |
it["should have a null function"] = function() | |
assert(__.null() == nil) | |
assert(__.null(27) == nil) | |
end | |
it["should have an identity function"] = function() | |
assert(__.identity(27) == 27) | |
assert(__.identity(42) == 42) | |
end | |
it["should have a constant function"] = function() | |
assert(__.constant(27)()==27) | |
assert(__.constant(42)(27)==42) | |
end | |
it["should deepcopy and handle circular references"] = function() | |
local loopedobj = { child = { member = 42 }, member = 27} | |
loopedobj.child.loop = loopedobj | |
loopedobj.child.self = loopedobj.child | |
local copy = __.deepclone(loopedobj) | |
assert(copy.child.loop == copy) | |
assert(copy.child.self == copy.child) | |
end | |
it["should partial apply with placeholders"] = function() | |
assert(__.partial(function(a, b, c) return a + b + c end, 1)(2, 3) == 6) | |
assert(__.partial(function(a, b, c) return a + b + c end, 1, 2)(3) == 6) | |
assert(__.partial(function(a, b, c) return a + b + c end, 1, 2, 3)() == 6) | |
local obj = { | |
a = 27, | |
method = __.partial(function(self, b) return self.a, b end, __, 42) | |
} | |
local a, b = obj:method() | |
assert(a == 27 and b == 42) | |
end | |
it["should curry with placeholders"] = function() | |
local fn = __.curry(function(a, b, c) return a + b + c end, 3) | |
local one = fn(1) | |
local two = one(2) | |
assert(two(3)==6 and one(2, 3)==6 and fn(1, 2, 3)==6) | |
assert(one(__, 3)(2)==6) | |
end | |
it["should handle any and all"] = function() | |
assert(__.any(__.op.eq, {1,2,3}, 3)==true) | |
assert(__.any(__.op.eq, {1,2,3}, 4)==false) | |
assert(__.all(__.op.gte, {1,2,3}, 1)==true) | |
assert(__.all(__.op.gte, {1,2,3}, 4)==false) | |
end | |
it["should zip lists as expected"] = function() | |
local zipped = __.zip({1}, {2, 4}, {3}) | |
assert(#zipped==2 and #zipped[1]==3) | |
end | |
it["should transform one object into another"] = function() | |
local object = { a = 1, b = 2, c = 3 } | |
local mutant = __.transform(function(acc, k, v) acc[k:upper()] = v * 2 end, object) | |
assert(mutant.A == 2 and mutant.C == 6) | |
end | |
it["should reduce some numbers into a number"] = function() | |
local sum = __.partial(__.reduce, __.op.add) | |
assert(sum({1, 2, 3}) == 6) | |
end | |
it["should compose functions together"] = function() | |
assert(__.pipe(function(a) return a + 1 end, function(b) return b + 2 end)(3)==6) | |
local chain = __.pipe( | |
__.zip, | |
__.flatten, | |
__.partial(__.filter, function(v) return v > 0 end), | |
__.partial(__.map, function(v) return v * 2 end), | |
__.partial(__.reduce, __.op.add)) | |
assert(chain({0, 2}, {1, 3}) == 12) | |
end | |
it["should count with a criteria"] = function() | |
assert(__.count(function(v) return v%2==1 end, {1, 2, 3, 4, 5}) == 3) | |
assert(__.count(function(v) return v%2==0 end, {1, 2, 3, 4, 5}) == 2) | |
end | |
it["should maybe not run functions when there's a nil parameter"] = function() | |
local fn = __.maybe(function(x) return x * 2 end) | |
assert(fn(2)==4) | |
assert(fn(nil)==nil) | |
local chain = __.pipe(__.maybe( | |
function(a) return a + 1 end, | |
function(b) return b + 2 end, | |
function(c) return c + 3 end, | |
function(d) return d + 4 end | |
)) | |
assert(chain(0)==10) | |
assert(chain(nil)==nil) | |
end | |
it["should do some string functions"] = function() | |
assert(__.trim(" A ")=="A") | |
assert(__.concatstr(" ", {"hi", "world"})=="hi world") | |
assert(__.concatstr({"hi", "world"})=="hiworld") | |
assert(__.concatstr(function(l,r) return l..':'..r end, {"hi", "world"})=="hi:world") | |
assert(__.concatstr({})=="") | |
assert(__.split("A B")[1]=="A") | |
assert(__.split(':', "A:B")[1]=="A") | |
assert(__.split('.', "A.B")[1]=="A") | |
assert(__.split(']', "A]B")[1]=="A") | |
assert(__.startsWith("hi", "hi mom!")) | |
assert(__.endsWith("mom!", "hi mom!")) | |
end | |
it["should only run a function once"] = function() | |
local one = 0 | |
local once = __.once(function() one = one + 1; return one end) | |
assert((once()==1) and (once()==1) and (one == 1)) | |
end | |
it["should memoize a function with a custom hash"] = function() | |
local doubles = __.memoize(function(x) return x * 2 end, | |
function(x) return math.floor(x/2)*2 end) | |
assert(doubles(0)==0) | |
assert(doubles(1)==0) | |
assert(doubles(2)==4) | |
assert(doubles(3)==4) | |
assert(doubles(4)==8) | |
assert(doubles(5)==8) | |
end | |
it["should compare elements in a list"] = function() | |
assert(__.compare(__.op.eq, {1, 2, 3}, {1, 2, 3})) | |
assert(__.compare(__.op.neq, {1, 2, 3}, {4, 5, 6})) | |
assert(__.compare(__.op.lt, {2, 3, 4}, {3, 4, 5})) | |
assert(__.compare(__.op.gt, {2, 3, 4}, {1, 2, 3})) | |
assert(__.compare(__.op.lte, {1, 2, 3}, {1, 3, 5})) | |
assert(__.compare(__.op.gte, {2, 3, 4}, {2, 2, 2})) | |
end | |
it["should shuffle"] = function() | |
local src = {1, 2, 3, 4, 5} | |
local list = __.shuffle(math.random, src) | |
assert(list ~= src) -- referential transparency | |
for i = 1, #src do | |
assert(src[i]==i) -- immutability | |
end | |
end | |
it["should have working isType functions"] = function() | |
assert(__.isNil(nil) and not __.isNil(false)) | |
assert(__.isNumber(27) and not __.isNumber("hello")) | |
assert(__.isString("hello") and not __.isString(27)) | |
assert(__.isBoolean(true) and not __.isBoolean(27)) | |
assert(__.isTable({"hello"}) and not __.isTable("hello")) | |
assert(__.isFunction(__.null) and not __.isFunction(__)) | |
assert(__.isThread(coroutine.create(__.null)) and not __.isThread(__)) | |
end | |
it["should have a working range function"] = function() | |
local A = __.range() | |
for i = 1, 10 do assert(A()==i) end | |
A = __.range(5, 9, 2) | |
for i = 5, 9, 2 do assert(A()==i) end | |
A = __.range(-1, -10) | |
for i = -1, -10, -1 do assert(A()==i) end | |
A = __.range(2) | |
assert(A()==1) | |
assert(A()==2) | |
assert(A()==nil) | |
end | |
it["should have a working unique function"] = function() | |
local before = {1, 2, 5, 2, 3, 2, 4, 2, 2, 5, 3, 5} | |
assert(__.compare(__.op.eq, __.unique(__.op.eq, before), {1, 2, 5, 3, 4})) | |
local other = {{"A", "B"}, {"A", "C"}, {"A", "D"}} | |
assert(#(__.unique(function(l, r) return l[1]==r[1] end, other))==1) | |
assert(#(__.unique(function(l, r) return l[2]==r[2] end, other))==#other) | |
end | |
it["can min, max, and get extents"] = function() | |
local numbers = {7, 6, 1, 4, 5, 2, 8, 3} | |
assert(__.min(__.identity, numbers)==1) | |
assert(__.max(__.identity, numbers)==8) | |
local min, max = __.extents(__.identity, numbers) | |
assert(min == 1) | |
assert(max == 8) | |
local marx_bros = { | |
{ name='groucho', born=1890 }, | |
{ name='harpo', born=1888 }, | |
{ name='zeppo', born=1901 }, | |
{ name='chico', born=1887 }, | |
{ name='gummo', born=1892 }, | |
} | |
assert(__.min('born', marx_bros).name == 'chico') | |
assert(__.max('born', marx_bros).name == 'zeppo') | |
end | |
it["can take the head, rest, and tail of a list"] = function() | |
local list = {1, 2, 3, 4, 5} | |
local head, rest, tail = __.head(list), __.rest(list), __.tail(list) | |
assert(#head==1 and head[1]==1) | |
assert(#rest==4 and rest[2]==3) | |
assert(#tail==1 and tail[1]==5) | |
local other = __.rest(4, 5, list) | |
assert(#other==2 and other[1]==4) | |
end | |
it["allows for funkier submodules acting as funky's placeholder"] = function() | |
local funkier = setmetatable({}, {__index = __}) | |
__.ADD_FUNKY(funkier) | |
local sum = 0 | |
__.each(function(x) sum = sum + x; return funkier end, {1, 2}) | |
assert(sum == 1) | |
end | |
it["should modify objects via the extend function"] = function() | |
local object = { A = 1 } | |
local other = { A = 2, B = 3 } | |
local third = { C = 4 } | |
__.extend(object, other, third) | |
assert(object.A == 2) | |
assert(object.B == 3) | |
assert(object.C == 4) | |
assert(other.C == nil) | |
assert(third.A == nil) | |
assert(third.B == nil) | |
end | |
it["should produce anonymous functions via a lambda syntax"] = function() | |
local X = __.lambda 'x=>x*2' | |
assert(X and type(X)=="function") | |
assert(X(2)==4 and X(4)==8) | |
local Y = __.lambda "x, y -> x + y" | |
assert(Y and type(Y)=="function") | |
assert(Y(1,2)==3 and Y(5,2)==7) | |
local Z = __.lambda "...: select('#', ...)" | |
assert(Z and type(Z)=="function") | |
assert(Z(1)==1 and Z(1,1)==2 and Z(1,1,1)==3) | |
assert(#(__.filter(__.lambda 'x-=:>x>5', {1, 3, 5, 7, 9})) == 2) | |
local W = __.lambda 'x:=>__.is("number", x)' | |
assert(W(5)==true and W("hi")==false) | |
end | |
it["should concat, union, and intersect arrays"] = function() | |
local X = __.concat({1, 2, 3}, {2, 3, 4}) | |
assert(__.compare(__.op.eq, X, {1, 2, 3, 2, 3, 4})) | |
local Y = __.union({1, 2, 3}, {2, 3, 4}) | |
assert(__.compare(__.op.eq, Y, {1, 2, 3, 4})) | |
local Z = __.intersect({1, 2, 3}, {2, 3, 4}) | |
assert(__.compare(__.op.eq, Z, {2, 3})) | |
end | |
-------------------------------------------------------------------------------- | |
print("Funky unit tests."); | |
for description, run in pairs(it) do | |
if xpcall(run, function(...) | |
print("FAILED:", description) | |
print(' ', ...) | |
print(' ', debug.traceback()) | |
end) and full_messages then | |
print("Passed:", description) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment