Skip to content

Instantly share code, notes, and snippets.

@inmatarian
Last active June 13, 2016 23:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save inmatarian/5ea8da95491b25c8c362 to your computer and use it in GitHub Desktop.
Save inmatarian/5ea8da95491b25c8c362 to your computer and use it in GitHub Desktop.
latest version of funky.lua
-- 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 __
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