chore: initial rojo + roblox bundling support

This commit is contained in:
Erica Marigold 2024-05-24 23:30:33 +05:30
parent 376b7ae3a7
commit c42e4ad3fe
No known key found for this signature in database
GPG key ID: 2768CC0C23D245D1
16 changed files with 2375 additions and 25 deletions

12
.darklua.json5 Normal file
View file

@ -0,0 +1,12 @@
{
rules: [
{
rule: "convert_require",
current: "path",
target: {
name: "roblox",
indexing_style: "wait_for_child",
},
},
],
}

9
default.project.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "rusty-luau",
"tree": {
"$path": "rbx",
"deps": {
"$path": "deps.luau"
}
}
}

21
deps.luau Normal file
View file

@ -0,0 +1,21 @@
-- All types from https://github.com/ffrostflame/LuauSignal/blob/f8305cc6b/src/init.luau#L97-L110
return function(isRoblox: boolean, isLune: boolean)
local SIGNAL_PATH = "Packages/_Index/ffrostflame_luausignal@0.2.4/luausignal/src/init.luau"
local signal = if isRoblox
then require(script.Parent.Parent.luausignal :: ModuleScript)
elseif isLune then require("@lune/luau").load(
'local task = require("@lune/task")\n' .. require("@lune/fs").readFile(SIGNAL_PATH)
)()
else error("Unsupported runtime: " .. _VERSION)
local task = if isRoblox
then task
elseif isLune then require("@lune/task")
else error("Unsupported runtime: " .. _VERSION)
return {
signal = signal,
task = task,
}
end

17
dev.project.json Normal file
View file

@ -0,0 +1,17 @@
{
"name": "rusty-luau",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Packages": {
"$path": "Packages",
"$ignoreUnknownInstances": true,
"rusty-luau": {
"$path": "default.project.json",
"$ignoreUnknownInstances": true
}
}
}
}
}

View file

@ -7,13 +7,13 @@
would rather import this conversion module.
]]
local option = require("option")
local option = require("./option.luau")
local Option = option.Option
export type Option<T> = option.Option<T>
local None = option.None
local Some = option.Some
local result = require("result")
local result = require("./result.luau")
local Result = result.Result
export type Result<T, E> = result.Result<T, E>
local Ok = result.Ok

View file

@ -1,15 +1,26 @@
local task = require("@lune/task")
local isRoblox = _VERSION == "Luau"
local isLune = _VERSION:find("Lune") == 1
local mod = if isLune then require("../deps") else require(script.Parent.deps)
local requires = mod(isRoblox, isLune)
local mod = require("../mod")
local Signal = mod.signal
type Signal<T...> = mod.Signal<T...>
local Signal: {
new: <T...>() -> Signal<T...>,
} = requires.signal
type Signal<T...> = {
Fire: (self: Signal<T...>, T...) -> (),
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
Once: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
DisconnectAll: (self: Signal<T...>) -> (),
}
local result = require("result")
local task = requires.task
local result = require("./result")
type Result<T, E> = result.Result<T, E>
local Ok = result.Ok
local Err = result.Err
local option = require("option")
local option = require("./option")
type Option<T> = option.Option<T>
local None = option.None
local Some = option.Some
@ -86,7 +97,10 @@ local function _constructor<T>(fn: (Signal<()>, Signal<T, Status>) -> ())
_thread = coroutine.create(fn),
_spawnEvt = Signal.new(),
_retEvt = Signal.new(),
-- This is a hack to make luau realize that this object and
-- Future<T> are related
_retEvt = Signal.new() :: Signal<T | Result<T, string>, Status>,
_status = "initialized",
} :: Future<T>,
{

View file

@ -1,4 +1,4 @@
local tableEq = require("util").tableEq
local tableEq = require("./util.luau").tableEq
--[=[
@class Option

View file

@ -1,4 +1,4 @@
local tableEq = require("util").tableEq
local tableEq = require("./util.luau").tableEq
--[=[
@class Result

View file

@ -1,14 +0,0 @@
local luau = require("@lune/luau")
local fs = require("@lune/fs")
local SIGNAL_PATH = "Packages/_Index/ffrostflame_luausignal@0.2.4/luausignal/src/init.luau"
local _signal = require(SIGNAL_PATH)
export type Signal<T...> = _signal.luauSignal<T...>
local signal: {
new: <T...>() -> Signal<T...>,
} =
luau.load('local task = require("@lune/task")\n' .. fs.readFile(SIGNAL_PATH))()
return {
signal = signal,
}

210
rbx/conversion.luau Normal file
View file

@ -0,0 +1,210 @@
--[[
This exists because if the Option module were to have methods to convert
to Result, and the Result module were to have methods to convert to Option,
there would be a cyclic dependency.
So, if a consumer of this library wants to convert between the two, they
would rather import this conversion module.
]]
local option = require(script.Parent:WaitForChild('option'))
local Option = option.Option
export type Option<T> = option.Option<T>
local None = option.None
local Some = option.Some
local result = require(script.Parent:WaitForChild('result'))
local Result = result.Result
export type Result<T, E> = result.Result<T, E>
local Ok = result.Ok
local Err = result.Err
--[=[
@within Result
Converts from [Result]`<T, E>` to [Option]`<T>`.
Converts `self` into an [Option]`<T>`, and discarding the error, if any.
```lua
local x: Result<number, string> = Ok(2)
assert(x:ok() == Some(2))
x = Err("Nothing here")
assert(x:ok() == None())
```
@param self Result<T, E>
@return Option<T>
]=]
function Result.ok<T, E>(self: Result<T, E>): Option<T>
if self:isOk() then
if self._value == nil then
return None()
end
return Some(self._value)
end
return None()
end
--[=[
@within Result
Converts from [Result]`<T, E>` to [Option]`<E>`.
Converts `self` into an [Option]`<E>`, and discarding the success value, if any.
```lua
local x: Result<number, string> = Ok(2)
assert(x:ok() == Some(2))
x = Err("Nothing here")
assert(x:ok() == None())
```
@param self Result<T, E>
@return Option<E>
]=]
function Result.err<T, E>(self: Result<T, E>): Option<E>
if self:isErr() then
return Option.new(self._error) :: Option<E>
end
return None()
end
--[=[
@within Result
Transposes a [Result] of an [Option] into an [Option] of a [Result].
[Result:Ok]\([Option:None]\) will be mapped to [Option:None].
[Result:Ok]\([Option:Some]`(_)`\) and [Result:Err]\(`_`\) will be mapped to
[Option:Some]\([Result:Ok]`(_)`\) and [Option:Some]\([Option:Err]`(_)`\).
```lua
type SomeErr = {}
local x: Result<Option<number>, SomeErr> = Ok(Some(2))
local y: Option<Result<number, SomeErr>> = Some(Ok(2))
assert(x:transpose() == y)
```
@param self Result<Option<T>, E>
@return Option<Result<T, E>>
]=]
function Result.transpose<T, E>(self: Result<Option<T>, E>): Option<Result<T, E>>
if self._value == None() then
return None()
elseif self:isOkAnd(function(val): boolean
return val._optValue == nil
end) then
return Some(Ok(self._value._optValue))
elseif self:isErr() then
return Some(Err(self._error))
end
error("`Result` is not transposable")
end
--[=[
@within Option
Transforms the [Option]`<T>` into a [Result]`<T, E>`, mapping [Option:Some]`(v)`
to [Result:Ok]`(v)` and [Option:None] to [Result:Err]`(err)`.
Arguments passed to [Option:okOr] are eagerly evaluated; if you are passing the result
of a function call, it is recommended to use [Option:okOrElse], which is lazily evaluated.
```lua
local x: Option<string> = Some("foo")
assert(x:okOr(0) == Ok("foo"))
x = None()
assert(x:okOr(0) == Err(0))
```
@param self Option<T>
@param err E
@return Result<T, E>
]=]
function Option.okOr<T, E>(self: Option<T>, err: E): Result<T, E>
if self:isSome() then
return Ok(self._optValue)
end
return Err(err)
end
--[=[
@within Option
Transforms the [Option]`<T>` into a [Result]`<T, E>`, mapping [Option:Some]`(v)` to
[Result:Ok]`(v)` and [Option:None] to [Result:Err]`(err())`.
```lua
local x: Option<string> = Some("foo")
assert(x:okOrElse(function() return 0 end) == Ok("foo"))
x = None()
assert(x:okOrElse(function() return 0 end) == Err(0))
```
@param self Option<T>
@param err () -> E
@return Result<T, E>
]=]
function Option.okOrElse<T, E>(self: Option<T>, err: () -> E): Result<T, E>
if self:isSome() then
return Ok(self._optValue :: T)
end
return Err(err())
end
--[=[
@within Option
Transposes a [Option] of an [Result] into an [Result] of a [Option].
[Option:None] will be mapped to [Result:Ok]\([Option:None]\).
[Option:Some]\([Result:Ok]`(_)`\) and [Option:Some]\([Result:Err]\(`_`\)\) will
be mapped to [Result:Ok]\([Option:Some]`(_)`\) and [Result:Err]`(_)`.
```lua
type SomeErr = {}
local x: Result<Option<number>, SomeErr> = Ok(Some(5))
local y: Option<Result<number, SomeErr>> = Some(Ok(5))
assert(x == y:transpose())
```
@param self Option<Result<T, E>>
@return Result<Option<T>, E>
]=]
function Option.transpose<T, E>(self: Option<Result<T, E>>): Result<Option<T>, E>
if self:isSome() then
local inner = self._optValue
assert(self.typeId == "Option" and inner.typeId == "Result", "Only an `Option` of a `Result` can be transposed")
if inner:isOk() then
return Some(Ok(inner._value))
elseif inner:isErr() then
return Some(Err(inner._error))
end
end
return Ok(None())
end
return {
Ok = Ok,
Err = Err,
Result = Result,
Some = Some,
None = None,
Option = Option,
}

224
rbx/future.luau Normal file
View file

@ -0,0 +1,224 @@
local isRoblox = _VERSION == "Luau"
local isLune = _VERSION:find("Lune") == 1
local mod = if isLune then require(script.Parent.Parent:WaitForChild('deps'))else require(script.Parent.deps)
local requires = mod(isRoblox, isLune)
local Signal: {
new: <T...>() -> Signal<T...>
} = requires.signal
type Signal<T...> = {
Fire: (self: Signal<T...>,T...) -> (),
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
Once: (self: Signal<T...>, callback: (T...) -> ()) -> () -> (),
DisconnectAll: (self: Signal<T...>) -> ()
}
local task = requires.task
local result = require(script.Parent:WaitForChild('result'))
type Result<T, E> = result.Result<T, E>
local Ok = result.Ok
local Err = result.Err
local option = require(script.Parent:WaitForChild('option'))
type Option<T> = option.Option<T>
local None = option.None
local Some = option.Some
--[=[
@class Future
A future represents an asynchronous computation.
A future is a value that might not have finished computing yet. This kind of asynchronous value
makes it possible for a thread to continue doing useful work while it waits for the value to
become available.
### The [Future:poll] Method
The core method of future, poll, attempts to resolve the future into a final value. This method does
not block if the value is not ready. Instead, the current task is executed in the background, and
its progress is reported when polled. When using a future, you generally wont call poll directly,
but instead [Future:await] the value.
```lua
local net = require("@lune/net")
local fut: Future<Result<string, string>> = Future.try(function(url)
local resp = net.request({
url = url,
method = "GET",
})
assert(resp.ok)
return resp.body
end, { "https://jsonplaceholder.typicode.com/posts/1" })
local resp: Result<string, string> = fut:await()
print(net.jsonDecode(resp:unwrap()))
```
]=]
local Future = {}
--[=[
@private
@type Status "initialized" | "pending" | "cancelled" | "ready"
@within Future
Represents the status of a [Future].
]=]
export type Status = "initialized" | "pending" | "cancelled" | "ready"
--[=[
@private
@interface Future<T>
@within Future
Represents the internal state of a [Future].
@field _thread thread -- The background coroutine spawned for execution
@field _ret T -- The value returned once execution has halted
@field _spawnEvt Signal<()> -- Event for internal communication among threads pre execution
@field _retEvt Signal<T | Result<T, string>, Status> -- Event for internal communication among threads post execution
@field _status Status -- The status of the Future
]=]
export type Future<T> = typeof(Future) & {
_ret: T,
_thread: thread,
_spawnEvt: Signal<()>,
_retEvt: Signal<T | Result<T, string>, Status>,
_status: Status
}
local function _constructor<T>(fn: (Signal<()>, Signal<T, Status>) -> ())
return setmetatable(
{
_thread = coroutine.create(fn),
_spawnEvt = Signal.new(),
-- This is a hack to make luau realize that this object and
-- Future<T> are related
_retEvt = Signal.new() :: Signal<T | Result<T, string>, Status>,
_status = "initialized",
} :: Future<T>,
{
__index = Future,
}
)
end
--[=[
@within Future
Constructs a [Future] from a function to be run asynchronously.
:::caution
If a the provided function has the possibility to throw an error, instead of any
other rusty-luau types like [Result] or [Option], use [Future:try] instead.
:::
@param fn -- The function to be executed asynchronously
@param args -- The arguments table to be passed to to the function
@return Future<T> -- The constructed future
]=]
function Future.new<T>(fn: (...any) -> T, args: { any })
return _constructor(function(spawnEvt: Signal<()>, retEvt: Signal<T, Status>)
spawnEvt:Fire()
local ret = fn(table.unpack(args))
retEvt:Fire(ret, "ready")
end)
end
--[=[
@within Future
Constructs a fallible [Future] from a function to be run asynchronously.
@param fn -- The fallible function to be executed asynchronously
@param args -- The arguments table to be passed to to the function
@return Future<Result<T>> -- The constructed future
]=]
function Future.try<T>(fn: (...any) -> T, args: { any })
return _constructor(function(spawnEvt: Signal<()>, retEvt: Signal<Result<T, string>, Status>)
spawnEvt:Fire()
local ok, ret = pcall(fn, table.unpack(args))
local res: Result<T, string> = if ok then Ok(ret) else Err(ret)
retEvt:Fire(res, "ready")
end)
end
--[=[
@within Future
Polls a [Future] to completion.
@param self Future<T>
@return (Status, Option<T>) -- Returns the [Status] and an optional return if completed
]=]
function Future.poll<T>(self: Future<T>): (Status, Option<T>)
if self._status == "initialized" then
self._retEvt:Connect(function(firedRet, status: Status)
self._status = status
self._ret = firedRet
-- Cleanup
coroutine.yield(self._thread)
coroutine.close(self._thread)
self._spawnEvt:DisconnectAll()
self._retEvt:DisconnectAll()
end)
self._spawnEvt:Connect(function()
self._status = "pending"
end)
coroutine.resume(self._thread, self._spawnEvt, self._retEvt)
end
if self._status == "pending" then
-- Just wait a bit more for the signal to fire
task.wait(0.01)
end
local retOpt = if self._ret == nil then None() else Some(self._ret)
return self._status, retOpt
end
--[=[
@within Future
Cancels a [Future].
@param self Future<T>
]=]
function Future.cancel<T>(self: Future<T>)
self._retEvt:Fire(nil :: any, "cancelled")
self._status = "cancelled"
end
--[=[
@within Future
Suspend execution until the result of a [Future] is ready.
This method continuosly polls a [Future] until it reaches completion.
@param self Future<T>
@return T -- The value returned by the function on completion
]=]
function Future.await<T>(self: Future<T>): T
while true do
local status: Status, ret: Option<T> = self:poll()
if status == "ready" then
-- Safe to unwrap, we know it must not be nil
return ret:unwrap()
end
end
end
return Future

918
rbx/option.luau Normal file
View file

@ -0,0 +1,918 @@
local tableEq = require(script.Parent:WaitForChild('util')).tableEq
--[=[
@class Option
Type [Option] represents an optional value: every [Option] is either [Option:Some]
and contains a value, or [Option:None], and does not. Common uses of an [Option]
may involve:
* Initial values
* Return values for functions that are not defined over their entire input range (partial functions)
* Return value for otherwise reporting simple errors, where None is returned on error
* Optional object fields
* Values that can be loaned or taken
* Optional function arguments
```lua
function divide(numerator: number, denominator: number): Option<number>
if denominator == 0 then
None()
else
return Some(numerator / denominator)
end
end
```
]=]
local Option = {}
export type Option<T> = typeof(Option) & {
_optValue: T?,
typeId: "Option"
}
--[=[
@within Option
No value.
]=]
function None<T>(): Option<T>
return Option.new(nil) :: Option<T>
end
--[=[
@within Option
Some value of type `T`.
]=]
function Some<T>(val: T): Option<T>
return Option.new(val) :: Option<T>
end
--[=[
@within Option
Converts a potentially `nil` value into an [Option].
@param val T? -- The value to convert into an [Option]
@return Option<T>
]=]
function Option.new<T>(val: T?)
return setmetatable(
{
_optValue = val,
typeId = "Option",
} :: Option<T>,
{
__index = Option,
__tostring = function<T>(self: Option<T>)
if self._optValue == nil then
return `{self.typeId}::None`
else
return `{self.typeId}::Some({self._optValue})`
end
end,
__eq = function<T>(self: Option<T>, other: Option<T>): boolean
if typeof(self._optValue) == "table" and typeof(other._optValue) == "table" then
return tableEq(self._optValue, other._optValue)
else
return self._optValue == other._optValue
end
end,
__lt = function<T>(self: Option<T>, other: Option<T>): boolean
if self:isSome() and other:isSome() then
return (self._optValue :: any) < (other._optValue :: any)
end
return false
end,
__le = function<T>(self: Option<T>, other: Option<T>): boolean
if self:isSome() and other:isSome() then
return (self._optValue :: any) <= (other._optValue :: any)
end
return false
end,
}
-- TODO: Implement __iter, once iterators traits exist
)
end
--[=[
@within Option
Returns `true` is the [Option] is an [Option:Some] value.
@param self Option<T>
@return boolean
]=]
function Option.isSome<T>(self: Option<T>): boolean
return self._optValue ~= nil
end
--[=[
@within Option
Returns `true` is the [Option] is an [Option:Some] value and the value inside of it
matches a predicate.
@param self Option<T>
@param op (val: T) -> boolean -- The predicate function
@return boolean
]=]
function Option.isSomeAnd<T>(self: Option<T>, op: (val: T) -> boolean): boolean
-- We know that it's not None, so this is fine
return self:isSome() and op(self._optValue :: T)
end
--[=[
@within Option
Returns `true` is the [Option] is an [Option:None] value.
@param self Option<T>
@return boolean
]=]
function Option.isNone<T>(self: Option<T>): boolean
return not self:isSome()
end
--[=[
@within Option
Returns the [Option:Some].
@error panic -- Panics if there is an [Option:None] with a custom panic message provided by `msg`.
```lua
local x: Option<string> = Some("value")
assert(x:expect("fruits are healthy") == "value")
```
```lua
local x: Option<string> = None()
x:expect("fruits are healthy") -- panics with `fruits are healthy`
```
@param self Option<T>
@param msg string -- The panic message
@return boolean
]=]
function Option.expect<T>(self: Option<T>, msg: string): T | never
if self:isSome() then
return self._optValue :: T
end
return error(`panic: {msg}; expected Option to be type Option::Some(T), got Option::None`)
end
--[=[
@within Option
Returns the [Option:Some].
Because this function may panic, its use is generally discouraged. Instead, prefer to use
[Option:unwrapOr], [Option:unwrapOrElse], or [Option:unwrapOrDefault].
@error panic -- Panics if the self value is [Option:None].
@param self Option<T>
@return T | never
]=]
function Option.unwrap<T>(self: Option<T>): T | never
if self:isSome() then
return self._optValue :: T
end
return error("called `Option:unwrap()` on a `None` value")
end
--[=[
@within Option
Returns the contained [Option:Some] value or a provided default.
Arguments passed to unwrap_or are eagerly evaluated; if you are passing the result of
a function call, it is recommended to use [Option:unwrapOrElse], which is lazily
evaluated.
```lua
assert(Some("car"):unwrapOr("bike") == "car")
assert(None():unwrapOr("bike") == "bike")
```
@param self Option<T>
@param default T -- The default value
@return T
]=]
function Option.unwrapOr<T>(self: Option<T>, default: T): T
if self:isSome() then
return self._optValue :: T
end
return default
end
--[=[
@within Option
Returns the contained [Option:Some] value or computes it from a function.
```lua
local k = 10
assert(Some(4).unwrapOrElse(function()
return 2 * k
end) == 4)
assert(None().unwrapOrElse(function()
return 2 * k
end) == 20)
```
@param self Option<T>
@param default () -> T -- The function which computes the default value
@return T
]=]
function Option.unwrapOrElse<T>(self: Option<T>, default: () -> T): T
if self:isSome() then
return self._optValue :: T
end
return default()
end
--[=[
@within Option
Maps an [Option]<T> to [Option]<U> by applying a function to a contained value
(if [Option:Some]) or returns [Option:None](if [Option:None]).
```lua
local maybeSomeString: Option<string> = Some("Hello, World!")
local maybeSomeLen: Option<number> = maybeSomeString:map(function(s)
return #s
end)
assert(maybeSomeLen == 13)
local x: Option<string> = None()
assert(x:map(function(s)
return #s
end) == None())
```
@param self Option<T>
@param op (x: T) -> U? -- The function to apply
@return Option<U>
]=]
function Option.map<T, U>(self: Option<T>, op: (x: T) -> U?): Option<U>
if self:isSome() then
local val = op(self._optValue :: T)
if val == nil then
return None()
end
return Some(val)
end
return None()
end
--[=[
@within Option
Calls the provided closure with the contained value (if [Option:Some]).
```lua
local v = { 1, 2, 3, 4, 5 }
-- prints "got: 4"
local x: Option<number> = Option.new(v[4]):inspect(function(x)
print("got: " .. x)
end)
-- prints nothing
local x: Option<number> = Option.new(v[5]):inspect(function(x)
print("got: " .. x)
end)
```
@param self Option<T>
@param op (x: T) -> () -- The function to call
@return Option<T>
]=]
function Option.inspect<T>(self: Option<T>, op: (x: T) -> ()): Option<T>
if self:isSome() then
op(self._optValue :: T)
end
return self
end
--[=[
@within Option
Returns the provided default result (if none), or applies a function to
the contained value (if any).
Arguments passed to [Option:mapOr] are eagerly evaluated; if you are passing the
result of a function call, it is recommended to use [Option:mapOrElse],
which is lazily evaluated.
```lua
local x: Option<string> = Some("foo")
assert(x.mapOr(42, function(v) return #v end) == 3)
local x: Option<string> = None()
assert(x.mapOr(42, function(v) return #v end) == 42)
```
@param self Option<T>
@param default U -- The default value
@param op (val: T) -> U -- The function to apply
@return Option<T>
]=]
function Option.mapOr<T, U>(self: Option<T>, default: U, op: (val: T) -> U)
if self:isSome() then
return op(self._optValue :: T)
end
return default
end
--[=[
@within Option
Computes a default function result (if none), or applies a different function
to the contained value (if any).
```lua
local k = 21;
local x: Option<string> = Some("foo")
assert(
x:mapOrElse(
function() return 2 * k end,
function(v) return #v end
) == 3
)
local x: Option<string> = None()
assert(
x:mapOrElse(
function() return 2 * k end,
function(v) return #v end
) == 42
)
```
@param self Option<T>
@param default () -> U -- The function to compute the default value
@param op (val: T) -> U -- The function to apply
@return Option<T>
]=]
function Option.mapOrElse<T, U>(self: Option<T>, default: () -> U, op: (val: T) -> U): U
if self:isSome() then
return op(self._optValue :: T)
end
return default()
end
-- TODO: Iterator traits
function Option.iter(): never
return error("Unimplemented: `Option:iter()`")
end
--[=[
@within Option
Returns [Option:None] if the option is [Option:None], otherwise returns `optb`.
Arguments passed to and are eagerly evaluated; if you are passing the result of a
function call, it is recommended to use [Option:andThen], which is lazily evaluated.
```lua
local x: Option<number> = Some(2)
local y: Option<String> = None()
assert(x:and_(y) == None())
local x: Option<number> = None()
local y: Option<string> = Some("foo")
assert(x:and_(y) == None())
local x: Option<number> = Some(2)
local y: Option<string> = Some("foo")
assert(x:and_(y) == Some("foo"))
local x: Option<u32> = None()
local y: Option<string> = None()
assert(x:and_(y), None())
```
@param self Option<T>
@param optb Option<U> -- The other option
@return Option<U>
]=]
function Option.and_<T, U>(self: Option<T>, optb: Option<U>): Option<U>
if self:isSome() then
return optb
end
return None()
end
--[=[
@within Option
Returns [Option:None] if the option is [Option:None], otherwise calls `op` with the wrapped
value and returns the result.
Some languages call this operation flatmap.
```lua
function sqThenToString(x: number): Option<string>
return Option.new(x ^ 2):map(function(sq)
return tostring(sq)
end)
end
assert(Some(2):andThen(sqThenToString) == Some(tostring(4)))
assert(None():andThen(sqThenToString) == None())
```
Often used to chain fallible operations that may return [Option:None].
```lua
local arr2d = { { "A0", "A1" }, { "B0", "B1" } }
local item01: Option<string> = Option.new(arr2d[1]):andThen(function(row)
return row[2]
end)
assert(item01 == Some("A1"))
local item20: Option<string> = Option.new(arr2d[3]):andThen(function(row)
return row[0]
end)
assert(item20 == None())
```
@param self Option<T>
@param op (val: T) -> Option<U> -- The function to call
@return Option<U>
]=]
function Option.andThen<T, U>(self: Option<T>, op: (val: T) -> Option<U>): Option<U>
if self:isSome() then
return op(self._optValue :: T)
end
return None()
end
--[=[
@within Option
Returns [Option:None] if the option is [Option:None], otherwise calls `predicate` with
the wrapped value and returns:
* [Option:Some](t) if predicate returns `true` (where `t` is the wrapped value), and
* [Option:None] if predicate returns `false`.
```lua
function isEven(n: number): boolean
return n % 2 == 0
end
assert(None():filter(isEven) == None())
assert(Some(3):filter(isEven) == None())
assert(Some(4):filter(isEven) == Some(4))
```
@param self Option<T>
@param predicate (val: T) -> boolean -- The predicate function which must match an element
@return Option<T>
]=]
function Option.filter<T>(self: Option<T>, predicate: (val: T) -> boolean): Option<T>
if self:isSome() then
if predicate(self._optValue :: T) then
return self
end
end
return None()
end
--[=[
@within Option
Returns the option if it contains a value, otherwise returns `optb`.
Arguments passed to [Option:or_] are eagerly evaluated; if you are passing the result of
a function call, it is recommended to use [Option:orElse], which is lazily
evaluated.
```lua
local x: Option<number> = Some(2)
local y: Option<number> = None()
assert(x:or_(y) == Some(2))
local x: Option<number> = None()
local y: Option<number> = Some(100)
assert(x:or_(y) == Some(100))
local x: Option<number> = Some(2)
local y: Option<number> = Some(100)
assert(x:or_(y) == Some(2))
local x: Option<number> = None()
local y: Option<number> = None()
assert(x:or_(y), None())
```
@param self Option<T>
@param optb Option<T> -- The other option
@return Option<T>
]=]
function Option.or_<T>(self: Option<T>, optb: Option<T>): Option<T>
if self:isSome() then
return self
end
return optb
end
--[=[
@within Option
Returns the option if it contains a value, otherwise calls `op` and returns the result.
```lua
function nobody(): Option<string>
return None()
end
function vikings(): Option<string>
return Some("vikings")
end
assert(Some("barbarians"):orElse(vikings) == Some("barbarians"))
assert(None():orElse(vikings) == Some("vikings"))
assert(None():orElse(nobody) == None())
```
@param self Option<T>
@param op () -> Option<T> -- The function to call
@return Option<T>
]=]
function Option.orElse<T>(self: Option<T>, op: () -> Option<T>): Option<T>
if self:isSome() then
return self
end
return op()
end
--[=[
@within Option
Returns [Option:Some] if exactly one of `self`, `optb` is [Option:Some],
otherwise returns [Option:None].
```lua
local x: Option<number> = Some(2)
local y: Option<number> = None()
assert(x:xor(y) == Some(2))
local x: Option<number> = None()
local y: Option<number> = Some(2)
assert(x:xor(y) == Some(2))
local x: Option<number> = Some(2)
local y: Option<number> = Some(2)
assert(x:xor(y) == None())
local x: Option<number> = None()
local y: Option<number> = None()
assert(x:xor(y) == None())
```
@param self Option<T>
@param optb Option<T> -- The other option
@return Option<T>
]=]
function Option.xor<T>(self: Option<T>, optb: Option<T>): Option<T>
if self:isSome() and optb:isSome() then
return None()
end
if self:isSome() then
return self
elseif optb:isSome() then
return optb
end
return None()
end
--[=[
@within Option
Inserts value into the option, then returns it.
If the option already contains a value, the old value is dropped.
See also [Option:getOrInsert], which doesnt update the value if the
option already contains [Option:Some].
```lua
local opt: Option<number> = None()
local val: number = opt:insert(1)
assert(val == 1)
assert(opt:unwrap() == 1)
local val: number = opt:insert(2)
assert(val == 2)
```
@param self Option<T>
@param val T -- The value to insert
@return T
]=]
function Option.insert<T>(self: Option<T>, val: T): T
self._optValue = val
return self._optValue :: T
end
--[=[
@within Option
Inserts value into the option, then returns it.
If the option already contains a value, the old value is dropped.
See also [Option:getOrInsert], which doesnt update the value if the
option already contains [Option:Some].
```lua
local opt: Option<number> = None()
local val: number = opt:insert(1)
assert(val == 1)
assert(opt:unwrap() == 1)
local val: number = opt:insert(2)
assert(val == 2)
```
@param self Option<T>
@param val T -- The value to insert
@return T
]=]
function Option.getOrInsert<T>(self: Option<T>, val: T): T
if self:isNone() then
self._optValue = val
end
return self._optValue :: T
end
--[=[
@within Option
Takes the value out of the option, leaving an [Option:None] in its place.
```lua
local x: Option<number> = Some(2)
local y: Option<number> = x.take()
assert(x == None())
assert(y == Some(2))
local x: Option<number> = None()
local y: Option<number> = x.take()
assert(x == None())
assert(y == None())
```
@param self Option<T>
@return Option<T>
]=]
function Option.take<T>(self: Option<T>): Option<T>
if self:isSome() then
local val = self._optValue :: T
self._optValue = nil
return Some(val)
end
return None()
end
--[=[
@within Option
Replaces the actual value in the option by the value given in parameter, returning
the old value if present, leaving an [Option:Some] in its place without
deinitializing either one.
```lua
local x: Option<number> = Some(2)
local old: Option<number> = x:replace(5)
assert(x == Some(5))
assert(old == Some(2))
local x: Option<number> = None()
local old: Option<number> = x:replace(3)
assert(x == Some(3))
assert(old == None())
```
@param self Option<T>
@param val T
@return Option<T>
]=]
function Option.replace<T>(self: Option<T>, val: T): Option<T>
local current: Option<T> = self
self._optValue = val
if current:isNone() then
return current
end
return Some(current._optValue :: T)
end
--[=[
@within Option
Returns true if `val` is contained in the [Option:Some].
```lua
local x: Option<number> = Some(2)
local y: Option<number> = None()
assert(x:contains(2))
assert(x:contains(4))
assert(not y:contains(2))
```
@param self Option<T>
@param val T
@return boolean
]=]
function Option.contains<T>(self: Option<T>, val: T): boolean
if self:isSome() then
return self._optValue == val
end
return false
end
--[=[
@within Option
Zips `self` with another [Option].
If `self` is [Option:Some](s) and other is [Option:Some](o), this method returns
[Option:Some]({s, o}). Otherwise, [Option:None] is returned.
```lua
local x: Option<number> = Some(1)
local y: Option<string> = Some("hi")
local z: Option<number> = None()
assert(x:zip(y) == Some({ 1, "hi" }))
assert(x:zip(z) == None())
```
@param self Option<T>
@param other Option>U>
@return Option<{T | U}>
]=]
function Option.zip<T, U>(self: Option<T>, other: Option<U>): Option<{ T | U }>
if self:isSome() and other:isSome() then
return Some({ self._optValue, other._optValue })
end
return None()
end
--[=[
@within Option
Zips `self` and another [Option] with function `op`.
If `self` is [Option:Some](s) and other is [Option:Some](o), this method returns
[Option:Some](op(s, o)). Otherwise, [Option:None] is returned.
```lua
type Point = {
x: number,
y: number,
}
local Point: Point & {
new: (x: number, y: number) -> Point,
} = {}
function Point.new(x: number, y: number): Point
return {
x = x,
y = y,
}
end
local xCoord: Option<number> = Some(17.5)
local yCoord: Option<number> = Some(42.7)
assert(xCoord:zipWith(yCoord, Point.new), Some({ x = 17.5, y = 42.7 }))
assert(x:zipWith(None(), Point.new), None())
```
@param self Option<T>
@param other Option>U>
@param op (x: T, y: U) -> R?
@return Option<R>
]=]
function Option.zipWith<T, U, R>(self: Option<T>, other: Option<U>, op: (x: T, y: U) -> R?): Option<R>
if self:isSome() and other:isSome() then
local computed = op(self._optValue :: T, other._optValue :: U)
if computed ~= nil then
return Some(computed :: R)
end
end
return None()
end
--[=[
@within Option
Unzips an option containing a table of two options.
If `self` is `Some({a, b})` this method returns `(Some(a), Some(b))`.
Otherwise, `(None(), None())` is returned.
```lua
local x: Option<{ number | string }> = Some({ 1, "hi" })
local y: Option<{ number }> = None()
assert((x:unzip() == Some(1), Some("hi")))
assert((y:unzip() == None(), None()))
```
@param self Option<T>
@return (Option<A>, Option<B>)
]=]
function Option.unzip<T, A, B>(self: Option<T>): (Option<A>, Option<B>)
if self:isSome() then
if self:isSome() and typeof(self._optValue) == "table" and #self._optValue == 2 then
return Some(self._optValue[1] :: A), Some(self._optValue[2] :: B)
end
end
return None(), None()
end
--[=[
@within Option
Returns the inner value wrapped by the [Option].
```lua
local x: Option<string> = Some("lol")
local y: Option<string> = None()
assert(x:unwrapUnchecked() == "lol")
assert(y:unwrapUnchecked() == nil)
```
@param self Option<T>
@return T?
]=]
function Option.unwrapUnchecked<T>(self: Option<T>): T?
return self._optValue
end
--[=[
@within Option
Returns a formatted representation of the option, often
used for printing to stdout.
```lua
local x: Option<number> = Some(123)
local y: Option<number> = None()
print(x:display()) -- prints `Option::Some(123)`
print(y:display()) -- prints `Option::None`
```
@param self Option<T>
@return string
]=]
function Option.display<T>(self: Option<T>): string
return tostring(self)
end
return {
Option = Option,
Some = Some,
None = None,
}

900
rbx/result.luau Normal file
View file

@ -0,0 +1,900 @@
local tableEq = require(script.Parent:WaitForChild('util')).tableEq
--[=[
@class Result
Error handling with the [Result] type.
[Result]`<T, E>` is the type used for returning a possible error. It is a class with the
constructor variants, [Result:Ok]`(T)`, representing success and containing a value, and
[Result:Err]`(E)`, representing error and containing an error value.
```lua
local result: Result<number, string>
-- Both variants `Ok` and `Err` fit into the type
result = Ok(21)
result = Err("some error")
```
Functions return [Result] whenever errors are expected and recoverable.
A simple function returning [Result] might be defined and used like so:
```lua
local PAT = "v(%d+)"
function parseVersion(str: string): Result<number, string>
local version: number = string.match(str, PAT)
if version == 0 then
return Err("Version cannot be zero")
else if version == nil then
return Err("Invalid string")
end
return Ok(version)
end
print("Successful result: " .. parseVersion("v12"):display())
print("Error result: " .. parseVersion("vString"):display())
```
[Result] comes with some convenience methods that make working with it more succinct.
```lua
local goodResult: Result<number, number> = Ok(10)
local badResult: Result<number, number> = Err(10)
-- The `isOk`and `isErr` methods do what they say.
assert(goodResult:isOk() and not goodResult:isErr())
assert(badResult:isErr() and not badResult:isOk())
-- `map` produces a new result from an existing one
goodResult = goodResult:map(function(i) return i + 1 end)
badResult = badResult:map(function(i) return i - 1 end)
-- Use `and_then` to continue the computation.
local anotherGoodResult: Result<boolean, number> = goodResult:andThen(function(i) return Ok(i == 11) end)
-- Use `or_else` to handle the error
local badResult: Result<number, number> = bad_result:orElse(function(i) return Ok(i + 20) end)
-- Get the internal `Ok` value and panic if there is an `Err`
local finalAwesomeResult: boolean = anotherGoodResult:unwrap()
```
]=]
local Result = {}
export type Result<T, E> = typeof(Result) & {
_value: T,
_error: E,
typeId: "Result"
}
--[=[
@within Result
Contains the success value.
@param val T
@return Result<T, E>
]=]
function Ok<T, E>(val: T): Result<T, E>
return Result.new(val, (nil :: unknown) :: E)
end
--[=[
@within Result
Contains the error value.
@param err E
@return Result<T, E>
]=]
function Err<T, E>(err: E): Result<T, E>
return Result.new((nil :: unknown) :: T, err)
end
--[=[
@within Result
@private
Constructs a [Result]. **For internal use only**.
For user consumption, see [Result:Ok] and [Result:Err].
@param err E
@return Result<T, E>
]=]
function Result.new<T, E>(val: T, err: E)
return setmetatable(
{
_value = val,
_error = err,
typeId = "Result",
} :: Result<T, E>,
{
__index = Result,
__tostring = function<T, E>(self: Result<T, E>)
if self:isOk() then
return `{self.typeId}::Ok({self._value})`
end
if self:isErr() then
return `{self.typeId}::Err({self._error})`
end
return `{self.typeId}<T, E>`
end,
__eq = function<T, E>(self: Result<T, E>, other: Result<T, E>)
if
typeof(self._value) ~= "table"
and typeof(self._error) ~= "table"
and typeof(other._value) ~= "table"
and typeof(other._error) ~= "table"
then
return self._value == other._value and self._error == other._error
end
return tableEq(self._value, other._value) and tableEq(self._error, other._error)
end,
-- TODO: Implement equality and arithmetic metamethods
-- TODO: Implement __iter, once iterators traits exist
}
)
end
--[=[
@within Result
Returns `true` if the result is [Result:Ok].
```lua
local x: Result<number, string> = Ok(3)
assert(x:isOk())
x = Err("Some error message")
assert(not x:isOk())
```
@param self Result<T, E>
@return boolean
]=]
function Result.isOk<T, E>(self: Result<T, E>): boolean
if self._value == nil then
return false
end
return true
end
--[=[
@within Result
Returns `true` if the result is [Result:Ok] and the value inside it matches a
predicate.
```lua
local x: Result<number, string> = Ok(2)
assert(x:isOkAnd(function(x) return x > 1 end))
x = Ok(0)
assert(not x:isOkAnd(function(x) return x > 1 end))
x = Err("hey")
assert(not x:isOkAnd(function(x) return x > 1 end))
```
@param self Result<T, E>
@param predicate (val: T?) -> boolean
@return boolean
]=]
function Result.isOkAnd<T, E>(self: Result<T, E>, predicate: (val: T?) -> boolean): boolean
return self:isOk() and predicate(self._value)
end
--[=[
@within Result
Returns `true` if the result is [Result:Err].
```lua
local x: Result<number, string> = Ok(3)
assert(not x:isErr())
x = Err("Some error message")
assert(x:isErr())
```
@param self Result<T, E>
@return boolean
]=]
function Result.isErr<T, E>(self: Result<T, E>)
return not self:isOk()
end
--[=[
@within Result
Returns `true` if the result is [Result:Err] and the value inside it matches a
predicate.
```lua
local x: Result<number, string> = Ok(2)
assert(not x:isErrAnd(function(x) return x > 1 end))
x = Err(3)
assert(x:isErrAnd(function(x) return x > 1 end))
x = Err("hey")
assert(not x:isErrAnd(function(x) return x > 1 end))
```
@param self Result<T, E>
@param predicate (val: T?) -> boolean
@return boolean
]=]
function Result.isErrAnd<T, E>(self: Result<T, E>, predicate: (val: E) -> boolean): boolean
if self:isErr() then
return predicate(self._error)
end
return false
end
--[=[
@within Result
Maps a [Result]<T, E> to [Result]<U, E> by applying a function to a contained [Result:Ok] value,
leaving an [Result:Err] value untouched.
This function can be used to compose the results of two functions.
```lua
local lines = "1\n2\n3\n4\n"
function parseInt(x: string): Result<number, string>
local num = tonumber(x)
if num == nil then
return Err("not an integer")
end
return Ok(num)
end
for line in lines:split("\n") do
local number = parseInt(line)
if number:isOk() then
print(number:unwrap())
else
print("not a number!")
end
end
```
@param self Result<T, E>
@param op (val: T) -> U
@return Result<U, E>
]=]
function Result.map<T, E, U>(self: Result<T, E>, op: (val: T) -> U): Result<U, E>
if self:isOk() then
return Result.new(op(self._value), self._error) :: Result<U, E>
end
return Err(self._error)
end
--[=[
@within Result
Returns the provided default (if [Result:Err]), or applies a function to
the contained value (if [Result:Ok]).
Arguments passed to [Result:mapOr] are eagerly evaluated; if you are passing the
result of a function call, it is recommended to use [Result:mapOrElse], which is
lazily evaluated.
```lua
local x: Result<string, string> = Ok("foo")
assert(x:mapOr("bar", string.upper) == "FOO")
x = Err("foo")
assert(x:mapOr("bar", string.upper) == "bar")
```
@param self Result<T, E>
@param default U
@param op (val: T) -> U
@return U
]=]
function Result.mapOr<T, E, U>(self: Result<T, E>, default: U, op: (val: T) -> U): U
if self:isOk() then
return op(self._value)
end
return default
end
--[=[
@within Result
Maps a [Result]<T, E> to U by applying fallback function default to a contained
[Result:Err] value, or function f to a contained [Result:Ok] value.
This function can be used to unpack a successful result while handling an error.
```lua
local x: Result<string, string> = Ok("foo")
assert(x:mapOrElse(function(x) return "error: ".. x end, string.upper) == "FOO")
x = Err("foo")
assert(x:mapOrElse(function(x) return "error: ".. x end, string.upper) == "error: foo")
```
@param self Result<T, E>
@param default (val: E) -> U
@param op (val: T) -> U
@return U
]=]
function Result.mapOrElse<T, E, U>(self: Result<T, E>, default: (val: E) -> U, op: (val: T) -> U): U
if self:isOk() then
return op(self._value)
end
return default(self._error)
end
--[=[
@within Result
Maps a [Result]<T, E> to [Result]<T, F> by applying a function to a contained
[Result:Err] value, leaving an [Result:Ok] value untouched.
This function can be used to pass through a successful result while handling an
error.
```lua
local function stringify(x: number): string
return string.format("error code: %d", x)
end
local x: Result<number, number> = Ok(2)
assert(x:mapErr(stringify) == Ok(2))
x = Err(13)
assert(x:mapErr(stringify) == Err("error code: 13"))
```
@param self Result<T, E>
@param op (val: E) -> F
@return Result<T, F>
]=]
function Result.mapErr<T, E, F>(self: Result<T, E>, op: (val: E) -> F): Result<T, F>
if self:isErr() then
return Result.new(self._value, op(self._error))
end
return Ok(self._value)
end
--[=[
@within Result
Calls the provided closure with a reference to the contained value
(if [Result:Ok]).
```lua
function parseInt(x: string): Result<number, string>
local num = tonumber(x)
if num == nil then
return Err("not an integer")
end
return Ok(num)
end
local x = parseInt("4")
:inspect(function(x) print("original: " .. x) end)
:map(function(x) return x ^ 3 end)
:expect("not an integer")
```
@param self Result<T, E>
@param op (val: E) -> ()
@return Result<T, E>
]=]
function Result.inspect<T, E>(self: Result<T, E>, op: (val: T) -> ()): Result<T, E>
if self:isOk() then
op(self._value)
end
return self
end
--[=[
@within Result
Calls the provided closure with a reference to the contained value
(if [Result:Err]).
```lua
function parseInt(x: string): Result<number, string>
local num = tonumber(x)
if num == nil then
return Err("not an integer")
end
return Ok(num)
end
local x = parseInt("string")
:inspectErr(function(x) print("error: " .. x) end)
:map(function(x) return x ^ 3 end)
:unwrap()
```
@param self Result<T, E>
@param op (val: E) -> ()
@return Result<T, E>
]=]
function Result.inspectErr<T, E>(self: Result<T, E>, op: (val: E) -> ()): Result<T, E>
if self:isErr() then
op(self._error)
end
return self
end
-- TODO: Iterator traits
-- selene: allow(unused_variable)
function Result.iter<T, E>(self: Result<T, E>): never
return error("Unimplemented: `Result:iter()`")
end
--[=[
@within Result
Returns the contained [Result:Ok] value, consuming the self value.
Because this function may panic, its use is generally discouraged. Instead,
prefer to use [Result:isErr] and handle the Err case explicitly, or call
[Result:unwrapOr], [Result:unwrapOrElse], or [Result:unwrapOrDefault].
Panics
@error panic -- If the value is a [Result:Err], with a panic message including the passed message, and the content of the `Err`.
```lua
local x: Result<number, string> = Err("emergency failure")
x:expect("Testing expect") -- panics with message `Testing expect: emergency failure`
```
## Recommended Message Style
It is recommended that expect messages are used to describe the reason you expect the
[Result] should be [Result:Ok].
```lua
local process = require("@lune/process")
local function envVar<T>(var: string): Result<T, string>
local val = process.env[var]
if val == nil then
return Err("environment variable not found")
end
Ok(val)
end
local path = envVar("IMPORTANT_PATH")
:expect("env variable `IMPORTANT_PATH` should be set by `wrapper_script.sh`")
```
**Hint**: If youre having trouble remembering how to phrase expect error messages remember to focus
on the word should as in env variable should be set by blah or the given binary should be available
and executable by the current user.
@param self Result<T, E>
@param msg string
@return T | never
]=]
function Result.expect<T, E>(self: Result<T, E>, msg: string): T | never
if self:isOk() then
return self._value
end
return error(`panic: {msg}; {self._error}`)
end
--[=[
@within Result
Returns the [Result:Ok].
Because this function may panic, its use is generally discouraged. Instead, prefer to use
[Result:unwrapOr], [Result:unwrapOrElse], or [Result:unwrapOrDefault].
@error panic -- Panics if the value is an [Result:Err], with a panic message provided by the [Result:Err]s value.
```lua
local x: Result<number, string> = Ok(2)
assert(x:unwrap() == 2)
x = Err("oh no")
x:unwrap() -- panics with `oh no`
```
@param self Result<T, E>
]=]
function Result.unwrap<T, E>(self: Result<T, E>): T | never
if self:isOk() then
return self._value
end
return error(`panic: \`Result:unwrap()\` called on an \`Err\` value: {self._error}`)
end
-- TODO: default values for types
-- selene: allow(unused_variable)
function Result.unwrapOrDefault<T, E>(self: Result<T, E>): never
return error("Unimplemented: `Result:unwrapOrDefault()`")
end
--[=[
@within Result
Returns the contained [Result:Err].
@error panic -- Panics if the value is an [Result:Ok], with a panic message including the passed message, and the content of the [Resul:Ok].
```lua
local x: Result<number, string> = Ok(10)
x:expectErr("Testing expect") -- panics with `Testing expect: 10`
```
@param self Result<T, E>
@param msg string
@return E | never
]=]
function Result.expectErr<T, E>(self: Result<T, E>, msg: string): E | never
if self:isErr() then
return self._error
end
return error(`panic: {msg}; {self._error}`)
end
--[=[
@within Result
Returns the contained [Result:Err].
@error panic -- Panics if the value is an [Result:Ok], with a panic message provided by the [Result:Ok]s value.
```lua
local x: Result<number, string> = Ok(2)
x:unwrapErr() -- panics with `2`
```
```lua
x = Err("oh no")
assert(x:unwrapErr() == "oh no")
```
@param self Result<T, E>
@return E | never
]=]
function Result.unwrapErr<T, E>(self: Result<T, E>): E | never
if self:isErr() then
return self._error
end
return error(`panic: \`Result:unwrapErr()\` called on an \`Ok\` value: {self._value}`)
end
-- TODO: How the fuck do I implement this?
-- selene: allow(unused_variable)
function Result.intoOk<T, E>(self: Result<T, E>): never
return error("Unimplemented: `Result:intoOk()`")
end
-- selene: allow(unused_variable)
function Result.intoErr<T, E>(self: Result<T, E>): never
return error("Unimplemented: `Result:intoErr()`")
end
--[=[
@within Result
Returns res if the result is [Result:Ok], otherwise returns the
[Result:Err] value of self.
```lua
local x: Result<number, string> = Ok(2)
local y: Result<string, string> = Err("late error")
assert(x:and_(y) == Err("late error"))
local x: Result<number, string> = Err("early error")
local y: Result<string, string> = Ok("foo")
assert(x:and_(y) == Err("early error"))
local x: Result<number, string> = Err("not a 2")
local y: Result<string, string> = Err("late error")
assert(x:and_(y) == Err("not a 2"))
local x: Result<number, string> = Ok(2)
local y: Result<string, string> = Ok("different result type")
assert(x:and_(y) == Ok("different result type"))
```
@param self Result<T, E>
@param res Result<U, E>
@return Result<U, E>
]=]
function Result.and_<T, E, U>(self: Result<T, E>, res: Result<U, E>): Result<U, E>
if self:isOk() then
return res
end
return Err(self._error)
end
--[=[
@within Result
Calls `op` if the result is [Result:Ok], otherwise returns the
[Result:Err] value of `self`.
This function can be used for control flow based on [Result]
values.
```lua
function sqThenToString(x): Result<string, string>
if typeof(sq) ~= "number" then
return Err("not a number: '" .. x .. "'")
end
return Ok(x ^ 2):map(function(sq)
return tostring(sq)
end)
end
assert(Ok(2):andThen(sqThenToString) == Ok("4"))
assert(Err("string"):andThen(sqThenToString) == Err("not a number: 'string'"))
```
Often used to chain fallible operations that may return [Result:Err].
@param self Result<T, E>
@param op (...any) -> any
@return Result<U, E>
]=]
function Result.andThen<T, E, U>(self: Result<T, E>, op: (...any) -> any): Result<U, E>
if self:isOk() then
return op(self._value)
end
return Err(self._error) :: Result<U, E>
end
--[=[
@within Result
Calls `op` if the result is [Result:Ok], otherwise returns the
[Result:Err] value of `self`.
This function can be used for control flow based on [Result]
values.
```lua
local x: Result<number, string> = Ok(2)
local y: Result<number, string> = Err("late error")
assert(x:or_(y) == Ok(2))
local x: Result<number, string> = Err("early error")
local y: Result<number, string> = Ok(2)
assert(x:or_(y) == Ok(2))
local x: Result<number, string> = Err("not a 2")
local y: Result<number, string> = Err("late error")
assert(x:or_(y) == Err("late error"))
local x: Result<number, string> = Ok(2)
local y: Result<number, string> = Ok(100)
assert(x:or_(y) == Ok(2))
```
@param self Result<T, E>
@param res Result<T, F>
@return Result<T, F> | never
]=]
function Result.or_<T, E, F>(self: Result<T, E>, res: Result<T, F>): Result<T, F> | never
if self:isErr() then
return res
end
if self:isOk() then
return Ok(self._value) :: Result<T, F>
end
return error("called `Result:or()` with an invalid value")
end
--[=[
@within Result
Calls `op` if the result is [Result:Ok], otherwise returns the
[Result:Err] value of `self`.
This function can be used for control flow based on [Result]
values.
```lua
function sq(x: number): Result<number, number>
return Ok(x * x)
end
function err(x: number): Result<number, number>
return Err(x)
end
assert(Ok(2):orElse(sq):orElse(sq) == Ok(2))
assert(Ok(2):orElse(err):orElse(sq) == Ok(2))
assert(Err(3):orElse(sq):orElse(err) == Ok(9))
assert(Err(3):orElse(err):orElse(err) == Err(3))
```
@param self Result<T, E>
@param op (x: E) -> Result<T, U>
@return Result<T, U>
]=]
function Result.orElse<T, E, U>(self: Result<T, E>, op: (x: E) -> Result<T, U>): Result<T, U>
if self:isErr() then
return op(self._error)
end
return Ok(self._value)
end
--[=[
@within Result
Returns the contained [Result:Ok] value or a provided default.
Arguments passed to [Result:unwrapOr] are eagerly evaluated; if you are passing the
result of a function call, it is recommended to use [Result:unwrapOrElse], which is
lazily evaluated.
```lua
local default = 2
local x: Result<number, string> = Ok(9)
assert(x:unwrapOr(default), 9)
x = Err("error")
assert(x:unwrapOr(default), default)
```
@param self Result<T, E>
@param default T
@return T
]=]
function Result.unwrapOr<T, E>(self: Result<T, E>, default: T): T
if self:isOk() then
return self._value
end
return default
end
--[=[
@within Result
Returns the contained [Result:Ok] value or computes it from a closure.
```lua
function count(x: string): number
return #x
end
assert(Ok(2):unwrapOrElse(count) == 2))
assert(Err("foo"):unwrapOrElse(count) == 3))
```
@param self Result<T, E>
@param op (x: E) -> T
@return T
]=]
function Result.unwrapOrElse<T, E>(self: Result<T, E>, op: (x: E) -> T): T
if self:isOk() then
return self._value
end
return op(self._error)
end
--[=[
@within Result
Returns true if the [Result:Ok] value is `val`.
```lua
local x: Result<number, string> = Ok(2)
assert(x:contains(2))
x = Ok(3)
assert(not x:contains(2))
x = Err(2)
assert(not x:contains(2))
```
@param self Result<T, E>
@param val T
@return boolean
]=]
function Result.contains<T, E>(self: Result<T, E>, val: T): boolean
if self:isOk() then
return self._value == val
end
return false
end
--[=[
@within Result
Returns true if the [Result:Err] value is `err`.
```lua
local x: Result<number, string> = Err(2)
assert(x:containsErr(2))
x = Err(3)
assert(not x:containsErr(2))
x = Ok(2)
assert(not x:containsErr(2))
```
@param self Result<T, E>
@param err E
@return boolean
]=]
function Result.containsErr<T, E>(self: Result<T, E>, err: E): boolean
if self:isErr() then
return self._error == err
end
return false
end
--[=[
@within Result
Returns a formatted representation of the result, often
used for printing to stdout.
```lua
local x: Result<number, string> = Ok(123)
local y: Result<number, string> = Err("error")
print(x:display()) -- prints `Result::Ok(123)`
print(y:display()) -- prints `Result::Err("error")`
```
@param self Result<T, E>
@return string
]=]
function Result.display<T, E>(self: Result<T, E>): string
return tostring(self)
end
return {
Ok = Ok,
Err = Err,
Result = Result,
}

38
rbx/util.luau Normal file
View file

@ -0,0 +1,38 @@
-- From https://gist.github.com/sapphyrus/fd9aeb871e3ce966cc4b0b969f62f539
local function tableEq(tbl1, tbl2)
if tbl1 == tbl2 then
return true
elseif type(tbl1) == "table" and type(tbl2) == "table" then
for key1, value1 in pairs(tbl1) do
local value2 = tbl2[key1]
if value2 == nil then
-- avoid the type call for missing keys in tbl2 by directly comparing with nil
return false
elseif value1 ~= value2 then
if type(value1) == "table" and type(value2) == "table" then
if not tableEq(value1, value2) then
return false
end
else
return false
end
end
end
-- check for missing keys in tbl1
for key2, _ in pairs(tbl2) do
if tbl1[key2] == nil then
return false
end
end
return true
end
return false
end
return {
tableEq = tableEq,
}

0
rusty-luau.luau Normal file
View file

1
sourcemap.json Normal file
View file

@ -0,0 +1 @@
{"name":"rusty-luau","className":"Folder","filePaths":["default.project.json"],"children":[{"name":"conversion","className":"ModuleScript","filePaths":["rbx/conversion.luau"]},{"name":"future","className":"ModuleScript","filePaths":["rbx/future.luau"]},{"name":"option","className":"ModuleScript","filePaths":["rbx/option.luau"]},{"name":"result","className":"ModuleScript","filePaths":["rbx/result.luau"]},{"name":"util","className":"ModuleScript","filePaths":["rbx/util.luau"]},{"name":"deps","className":"ModuleScript","filePaths":["deps.luau"]}]}