diff --git a/src/Functions.lua b/src/Functions.lua
index 4fee7ca..670aac6 100644
--- a/src/Functions.lua
+++ b/src/Functions.lua
@@ -1,9 +1,12 @@
--[[
Utility functions and building blocks for functional programming styles.
]]
+
local Tables = require(script.Parent.Tables)
local t = require(script.Parent.Parent.t)
+local optionalNumber = t.optional(t.number)
+
local Functions = {}
--[[
@@ -12,8 +15,7 @@ local Functions = {}
you don't want to do anything.
]]
--: () -> ()
-function Functions.noop()
-end
+function Functions.noop() end
--[[
A simple function that does nothing, but returns its input parameters.
@@ -36,9 +38,11 @@ end
]]
--: (...A -> () -> ...A)
function Functions.returns(...)
+ local length = select("#", ...)
local args = {...}
+
return function()
- return unpack(args)
+ return unpack(args, 1, length)
end
end
@@ -64,6 +68,7 @@ end
--: ((A -> R) -> A -> R)
function Functions.unary(fn)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
+
return function(first)
return fn(first)
end
@@ -78,7 +83,8 @@ end
]]
--: string -> () -> fail
function Functions.throws(errorMessage)
- assert(t.string(errorMessage), "BadInput: errorMessage must be a string")
+ assert(t.string(errorMessage))
+
return function()
error(errorMessage)
end
@@ -98,9 +104,11 @@ end
--: (((...A, ...A2 -> R), ...A) -> ...A2 -> R)
function Functions.bind(fn, ...)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
+ local length = select("#", ...)
local args = {...}
+
return function(...)
- return fn(unpack(args), ...)
+ return fn(unpack(args, 1, length), ...)
end
end
@@ -125,7 +133,7 @@ end
return player.Name
end)
local filterHurtNames = dash.compose(filterHurtPlayers, getName)
- filterHurtNames(game.Players) --> {"Frodo", "Boromir"}
+ filterHurtNames(game.Players) --> {"Frodo", "Boromir"}
@see `dash.filter`
@see `dash.compose`
@usage Chainable rodash function feeds are mapped to `dash.fn`, such as `dash.fn.map(handler)`.
@@ -133,9 +141,11 @@ end
--: (Chainable, ...) -> S -> S
function Functions.bindTail(fn, ...)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
+ local length = select("#", ...)
local args = {...}
+
return function(subject)
- return fn(subject, unpack(args))
+ return fn(subject, unpack(args, 1, length))
end
end
@@ -159,28 +169,28 @@ end
--: ((...A -> R), R?) -> Clearable & (...A -> R)
function Functions.once(fn)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
+
local called = false
local result = nil
local once = {
clear = function()
called = false
result = nil
- end
+ end,
}
- setmetatable(
- once,
- {
- __call = function(_, ...)
- if called then
- return result
- else
- called = true
- result = fn(...)
- return result
- end
+
+ setmetatable(once, {
+ __call = function(_, ...)
+ if called then
+ return result
+ else
+ called = true
+ result = fn(...)
+ return result
end
- }
- )
+ end,
+ })
+
return once
end
@@ -221,8 +231,8 @@ end
Chaining is useful when you want to simplify operating on data in a common form and perform
sequences of operations on some data with a very concise syntax. An _actor_ function can
check the value of the data at each step and change how the chain proceeds.
-
- Calling a _Chain_ with a subject reduces the chained operations in order on the subject.
+
+ Calling a _Chain_ with a subject reduces the chained operations in order on the subject.
@param actor called for each result in the chain to determine how the next operation should process it. (default = `dash.invoke`)
@example
-- Define a simple chain that can operate a list of numbers.
@@ -287,49 +297,52 @@ end
]]
--: {}>(T, Actor) -> Chain
function Functions.chain(fns, actor)
- if actor == nil then
- actor = Functions.invoke
- end
+ actor = actor or Functions.invoke
assert(Functions.isCallable(actor), "BadInput: actor must be callable")
local chain = {}
- setmetatable(
- chain,
- {
- __index = function(self, name)
- local fn = fns[name]
- assert(Functions.isCallable(fn), "BadFn: Chain key " .. tostring(name) .. " is not callable")
- local feeder = function(parent, ...)
- assert(type(parent) == "table", "BadCall: Chain functions must be called with ':'")
- local stage = {}
- local op = Functions.bindTail(fn, ...)
- setmetatable(
- stage,
- {
- __index = chain,
- __call = function(self, subject)
- local value = parent(subject)
- return actor(op, value)
- end,
- __tostring = function()
- return tostring(parent) .. "::" .. name
- end
- }
- )
- return stage
- end
- return feeder
- end,
- __newindex = function()
- error("ReadonlyKey: Cannot assign to a chain, create one with dash.chain instead.")
- end,
- __call = function(_, subject)
- return subject
- end,
- __tostring = function()
- return "Chain"
+
+ setmetatable(chain, {
+ __index = function(self, name)
+ local fn = fns[name]
+ assert(Functions.isCallable(fn), "BadFn: Chain key " .. tostring(name) .. " is not callable")
+
+ local feeder = function(parent, ...)
+ assert(t.table(parent))
+
+ local stage = {}
+ local op = Functions.bindTail(fn, ...)
+ setmetatable(stage, {
+ __index = chain,
+
+ __call = function(self, subject)
+ local value = parent(subject)
+ return actor(op, value)
+ end,
+
+ __tostring = function()
+ return tostring(parent) .. "::" .. name
+ end,
+ })
+
+ return stage
end
- }
- )
+
+ return feeder
+ end,
+
+ __newindex = function()
+ error("ReadonlyKey: Cannot assign to a chain, create one with dash.chain instead.")
+ end,
+
+ __call = function(_, subject)
+ return subject
+ end,
+
+ __tostring = function()
+ return "Chain"
+ end,
+ })
+
return chain
end
@@ -372,6 +385,7 @@ end
--: ((...A -> T -> R) -> T, ...A -> R)
function Functions.chainFn(fn)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
+
return function(source, ...)
return fn(...)(source)
end
@@ -399,7 +413,7 @@ end
An [Actor](/rodash/types#Actor) which cancels execution of a chain if a method returns nil, evaluating the chain as nil.
Can wrap any other actor which handles values that are non-nil.
- @example
+ @example
-- We can define a chain of Rodash functions that will skip after a nil is returned.
local maybeFn = dash.chain(_, dash.maybe())
local getName = function(player)
@@ -447,6 +461,7 @@ end
function Functions.maybe(actor)
actor = actor or Functions.invoke
assert(Functions.isCallable(actor), "BadInput: actor must be callable")
+
return function(fn, ...)
local args = {...}
if args[1] == nil then
@@ -463,7 +478,7 @@ end
This allows any asynchronous methods to be used in chains without modifying any of the chain's
synchronous methods, removing any boilerplate needed to handle promises in the main code body.
-
+
Can wrap any other actor which handles values after any promise resolution.
@param actor (default = `dash.invoke`) The actor to wrap.
@example
@@ -516,22 +531,19 @@ end
function Functions.continue(actor)
actor = actor or Functions.invoke
assert(Functions.isCallable(actor), "BadInput: actor must be callable")
+
return function(fn, value, ...)
local Async = require(script.Parent.Async)
- return Async.resolve(value):andThen(
- function(...)
- return actor(fn, ...)
- end
- )
+
+ return Async.resolve(value):andThen(function(...)
+ return actor(fn, ...)
+ end)
end
end
-local getRodashChain =
- Functions.once(
- function(rd)
- return Functions.chain(rd)
- end
-)
+local getRodashChain = Functions.once(function(rd)
+ return Functions.chain(rd)
+end)
--[[
A [Chain](/rodash/types/#chain) built from Rodash itself. Any
@@ -558,21 +570,20 @@ local getRodashChain =
]]
--: Chain
Functions.fn = {}
-setmetatable(
- Functions.fn,
- {
- __index = function(self, key)
- local rd = require(script.Parent)
- return getRodashChain(rd)[key]
- end,
- __call = function(self, subject)
- return subject
- end,
- __tostring = function()
- return "fn"
- end
- }
-)
+setmetatable(Functions.fn, {
+ __index = function(_, key)
+ local rd = require(script.Parent)
+ return getRodashChain(rd)[key]
+ end,
+
+ __call = function(_, subject)
+ return subject
+ end,
+
+ __tostring = function()
+ return "fn"
+ end,
+})
--[[
Returns a function that calls the argument functions in left-right order on an input, passing
@@ -595,12 +606,14 @@ function Functions.compose(...)
if fnCount == 0 then
return Functions.id
end
+
local fns = {...}
return function(...)
local result = {fns[1](...)}
for i = 2, fnCount do
result = {fns[i](unpack(result))}
end
+
return unpack(result)
end
end
@@ -641,6 +654,7 @@ function Functions.memoize(fn, serializeArgs)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
serializeArgs = serializeArgs or Functions.unary(Tables.serialize)
assert(Functions.isCallable(serializeArgs), "BadInput: serializeArgs must be callable or nil")
+
local cache = {}
local clearable = {
clear = function(_, ...)
@@ -649,26 +663,26 @@ function Functions.memoize(fn, serializeArgs)
cache[cacheKey] = nil
end
end,
+
clearAll = function()
cache = {}
- end
+ end,
}
- setmetatable(
- clearable,
- {
- __call = function(_, ...)
- local cacheKey = serializeArgs({...}, cache)
- if cacheKey == nil then
- return fn(...)
- else
- if cache[cacheKey] == nil then
- cache[cacheKey] = fn(...)
- end
- return cache[cacheKey]
+
+ setmetatable(clearable, {
+ __call = function(_, ...)
+ local cacheKey = serializeArgs({...}, cache)
+ if cacheKey == nil then
+ return fn(...)
+ else
+ if cache[cacheKey] == nil then
+ cache[cacheKey] = fn(...)
end
+ return cache[cacheKey]
end
- }
- )
+ end,
+ })
+
return clearable
end
@@ -677,7 +691,7 @@ end
The _fn_ is called in a separate thread, meaning that it will not block the thread it is called
in, and if the calling threads, the _fn_ will still be called at the expected time.
-
+
@returns an instance which `:clear()` can be called on to prevent _fn_ from firing.
@example
local waitTimeout = dash.setTimeout(function()
@@ -691,22 +705,22 @@ end
--: (Clearable -> ()), number -> Clearable
function Functions.setTimeout(fn, delayInSeconds)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
- assert(t.number(delayInSeconds), "BadInput: delayInSeconds must be a number")
+ assert(t.number(delayInSeconds))
+
local cleared = false
local timeout
- delay(
- delayInSeconds,
- function()
- if not cleared then
- fn(timeout)
- end
+ delay(delayInSeconds, function()
+ if not cleared then
+ fn(timeout)
end
- )
+ end)
+
timeout = {
clear = function()
cleared = true
- end
+ end,
}
+
return timeout
end
@@ -731,17 +745,20 @@ end
--: (Clearable -> ()), number, number? -> Clearable
function Functions.setInterval(fn, intervalInSeconds, delayInSeconds)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
- assert(t.number(intervalInSeconds), "BadInput: intervalInSeconds must be a number")
- assert(t.optional(t.number)(delayInSeconds), "BadInput: delayInSeconds must be a number")
+ assert(t.number(intervalInSeconds))
+ assert(optionalNumber(delayInSeconds))
+
local timeout
local callTimeout
local function handleTimeout()
callTimeout()
fn(timeout)
end
- callTimeout = function()
+
+ function callTimeout()
timeout = Functions.setTimeout(handleTimeout, intervalInSeconds)
end
+
if delayInSeconds ~= nil then
timeout = Functions.setTimeout(handleTimeout, delayInSeconds)
else
@@ -751,7 +768,7 @@ function Functions.setInterval(fn, intervalInSeconds, delayInSeconds)
return {
clear = function()
timeout:clear()
- end
+ end,
}
end
@@ -759,13 +776,13 @@ end
Creates a debounced function that delays calling _fn_ until after _delayInSeconds_ seconds have
elapsed since the last time the debounced function was attempted to be called.
@returns the debounced function with method `:clear()` can be called on to cancel any scheduled call.
- @usage A nice [visualisation of debounce vs. throttle](http://demo.nimius.net/debounce_throttle/),
+ @usage A nice [visualisation of debounce vs. throttle](http://demo.nimius.net/debounce_throttle/),
the illustrated point being debounce will only call _fn_ at the end of a spurt of events.
]]
--: (...A -> R), number -> Clearable & (...A -> R)
function Functions.debounce(fn, delayInSeconds)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
- assert(type(delayInSeconds) == "number", "BadInput: delayInSeconds must be a number")
+ assert(t.number(delayInSeconds))
local lastResult = nil
local timeout
@@ -775,27 +792,26 @@ function Functions.debounce(fn, delayInSeconds)
if timeout then
timeout:clear()
end
- end
+ end,
}
- setmetatable(
- debounced,
- {
- __call = function(_, ...)
- local args = {...}
- if timeout then
- timeout:clear()
- end
- timeout =
- Functions.setTimeout(
- function()
- lastResult = fn(unpack(args))
- end,
- delayInSeconds
- )
- return lastResult
+
+ setmetatable(debounced, {
+ __call = function(_, ...)
+ local length = select("#", ...)
+ local args = {...}
+
+ if timeout then
+ timeout:clear()
end
- }
- )
+
+ timeout = Functions.setTimeout(function()
+ lastResult = fn(unpack(args, 1, length))
+ end, delayInSeconds)
+
+ return lastResult
+ end,
+ })
+
return debounced
end
@@ -819,8 +835,7 @@ end
--: ((...A) -> R), number -> ...A -> R
function Functions.throttle(fn, cooldownInSeconds)
assert(Functions.isCallable(fn), "BadInput: fn must be callable")
- assert(type(cooldownInSeconds) == "number", "BadInput: cooldownInSeconds must be a number > 0")
- assert(cooldownInSeconds > 0, "BadInput: cooldownInSeconds must be a number > 0")
+ assert(t.numberPositive(cooldownInSeconds))
local cached = false
local lastResult = nil
@@ -828,13 +843,11 @@ function Functions.throttle(fn, cooldownInSeconds)
if not cached then
cached = true
lastResult = fn(...)
- Functions.setTimeout(
- function()
- cached = false
- end,
- cooldownInSeconds
- )
+ Functions.setTimeout(function()
+ cached = false
+ end, cooldownInSeconds)
end
+
return lastResult
end
end