From c42e4ad3fe4f59a9bb6bf16b75c9742f1479732b Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Fri, 24 May 2024 23:30:33 +0530 Subject: [PATCH] chore: initial rojo + roblox bundling support --- .darklua.json5 | 12 + default.project.json | 9 + deps.luau | 21 + dev.project.json | 17 + lib/conversion.luau | 4 +- lib/future.luau | 28 +- lib/option.luau | 2 +- lib/result.luau | 2 +- mod.luau | 14 - rbx/conversion.luau | 210 ++++++++++ rbx/future.luau | 224 +++++++++++ rbx/option.luau | 918 +++++++++++++++++++++++++++++++++++++++++++ rbx/result.luau | 900 ++++++++++++++++++++++++++++++++++++++++++ rbx/util.luau | 38 ++ rusty-luau.luau | 0 sourcemap.json | 1 + 16 files changed, 2375 insertions(+), 25 deletions(-) create mode 100644 .darklua.json5 create mode 100644 default.project.json create mode 100644 deps.luau create mode 100644 dev.project.json delete mode 100644 mod.luau create mode 100644 rbx/conversion.luau create mode 100644 rbx/future.luau create mode 100644 rbx/option.luau create mode 100644 rbx/result.luau create mode 100644 rbx/util.luau create mode 100644 rusty-luau.luau create mode 100644 sourcemap.json diff --git a/.darklua.json5 b/.darklua.json5 new file mode 100644 index 0000000..d9abb98 --- /dev/null +++ b/.darklua.json5 @@ -0,0 +1,12 @@ +{ + rules: [ + { + rule: "convert_require", + current: "path", + target: { + name: "roblox", + indexing_style: "wait_for_child", + }, + }, + ], +} \ No newline at end of file diff --git a/default.project.json b/default.project.json new file mode 100644 index 0000000..5871c4f --- /dev/null +++ b/default.project.json @@ -0,0 +1,9 @@ +{ + "name": "rusty-luau", + "tree": { + "$path": "rbx", + "deps": { + "$path": "deps.luau" + } + } +} \ No newline at end of file diff --git a/deps.luau b/deps.luau new file mode 100644 index 0000000..698eeff --- /dev/null +++ b/deps.luau @@ -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 diff --git a/dev.project.json b/dev.project.json new file mode 100644 index 0000000..05ed0ab --- /dev/null +++ b/dev.project.json @@ -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 + } + } + } + } +} \ No newline at end of file diff --git a/lib/conversion.luau b/lib/conversion.luau index a55588b..e8c0dde 100644 --- a/lib/conversion.luau +++ b/lib/conversion.luau @@ -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 = option.Option local None = option.None local Some = option.Some -local result = require("result") +local result = require("./result.luau") local Result = result.Result export type Result = result.Result local Ok = result.Ok diff --git a/lib/future.luau b/lib/future.luau index b6d836b..f24740a 100644 --- a/lib/future.luau +++ b/lib/future.luau @@ -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 = mod.Signal +local Signal: { + new: () -> Signal, +} = requires.signal +type Signal = { + Fire: (self: Signal, T...) -> (), + Connect: (self: Signal, callback: (T...) -> ()) -> () -> (), + Once: (self: Signal, callback: (T...) -> ()) -> () -> (), + DisconnectAll: (self: Signal) -> (), +} -local result = require("result") +local task = requires.task + +local result = require("./result") type Result = result.Result local Ok = result.Ok local Err = result.Err -local option = require("option") +local option = require("./option") type Option = option.Option local None = option.None local Some = option.Some @@ -86,7 +97,10 @@ local function _constructor(fn: (Signal<()>, Signal) -> ()) _thread = coroutine.create(fn), _spawnEvt = Signal.new(), - _retEvt = Signal.new(), + -- This is a hack to make luau realize that this object and + -- Future are related + _retEvt = Signal.new() :: Signal, Status>, + _status = "initialized", } :: Future, { diff --git a/lib/option.luau b/lib/option.luau index 4a20824..764f0ed 100644 --- a/lib/option.luau +++ b/lib/option.luau @@ -1,4 +1,4 @@ -local tableEq = require("util").tableEq +local tableEq = require("./util.luau").tableEq --[=[ @class Option diff --git a/lib/result.luau b/lib/result.luau index deca2dc..c8837c5 100644 --- a/lib/result.luau +++ b/lib/result.luau @@ -1,4 +1,4 @@ -local tableEq = require("util").tableEq +local tableEq = require("./util.luau").tableEq --[=[ @class Result diff --git a/mod.luau b/mod.luau deleted file mode 100644 index f5b03c6..0000000 --- a/mod.luau +++ /dev/null @@ -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 = _signal.luauSignal -local signal: { - new: () -> Signal, -} = - luau.load('local task = require("@lune/task")\n' .. fs.readFile(SIGNAL_PATH))() - -return { - signal = signal, -} diff --git a/rbx/conversion.luau b/rbx/conversion.luau new file mode 100644 index 0000000..9340cf4 --- /dev/null +++ b/rbx/conversion.luau @@ -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 = option.Option +local None = option.None +local Some = option.Some + +local result = require(script.Parent:WaitForChild('result')) +local Result = result.Result +export type Result = result.Result +local Ok = result.Ok +local Err = result.Err + +--[=[ + @within Result + + Converts from [Result]`` to [Option]``. + + Converts `self` into an [Option]``, and discarding the error, if any. + + ```lua + local x: Result = Ok(2) + assert(x:ok() == Some(2)) + + x = Err("Nothing here") + assert(x:ok() == None()) + ``` + + @param self Result + @return Option +]=] +function Result.ok(self: Result): Option + 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]`` to [Option]``. + + Converts `self` into an [Option]``, and discarding the success value, if any. + + ```lua + local x: Result = Ok(2) + assert(x:ok() == Some(2)) + + x = Err("Nothing here") + assert(x:ok() == None()) + ``` + + @param self Result + @return Option +]=] +function Result.err(self: Result): Option + if self:isErr() then + return Option.new(self._error) :: Option + 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, SomeErr> = Ok(Some(2)) + local y: Option> = Some(Ok(2)) + assert(x:transpose() == y) + ``` + + @param self Result, E> + @return Option> +]=] +function Result.transpose(self: Result, E>): Option> + 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]`` into a [Result]``, 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 = Some("foo") + assert(x:okOr(0) == Ok("foo")) + + x = None() + assert(x:okOr(0) == Err(0)) + ``` + + @param self Option + @param err E + @return Result +]=] +function Option.okOr(self: Option, err: E): Result + if self:isSome() then + return Ok(self._optValue) + end + + return Err(err) +end + +--[=[ + @within Option + + Transforms the [Option]`` into a [Result]``, mapping [Option:Some]`(v)` to + [Result:Ok]`(v)` and [Option:None] to [Result:Err]`(err())`. + + ```lua + local x: Option = 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 + @param err () -> E + @return Result +]=] +function Option.okOrElse(self: Option, err: () -> E): Result + 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, SomeErr> = Ok(Some(5)) + local y: Option> = Some(Ok(5)) + assert(x == y:transpose()) + ``` + + @param self Option> + @return Result, E> +]=] +function Option.transpose(self: Option>): Result, 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, +} diff --git a/rbx/future.luau b/rbx/future.luau new file mode 100644 index 0000000..12b81fa --- /dev/null +++ b/rbx/future.luau @@ -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: () -> Signal +} = requires.signal +type Signal = { + Fire: (self: Signal,T...) -> (), + Connect: (self: Signal, callback: (T...) -> ()) -> () -> (), + Once: (self: Signal, callback: (T...) -> ()) -> () -> (), + DisconnectAll: (self: Signal) -> () +} + +local task = requires.task + +local result = require(script.Parent:WaitForChild('result')) +type Result = result.Result +local Ok = result.Ok +local Err = result.Err + +local option = require(script.Parent:WaitForChild('option')) +type Option = option.Option +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 won’t call poll directly, + but instead [Future:await] the value. + + ```lua + local net = require("@lune/net") + + local fut: Future> = 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 = 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 + @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, Status> -- Event for internal communication among threads post execution + @field _status Status -- The status of the Future + +]=] +export type Future = typeof(Future) & { + _ret: T, + _thread: thread, + _spawnEvt: Signal<()>, + _retEvt: Signal, Status>, + _status: Status +} + +local function _constructor(fn: (Signal<()>, Signal) -> ()) + return setmetatable( + { + _thread = coroutine.create(fn), + + _spawnEvt = Signal.new(), + -- This is a hack to make luau realize that this object and + -- Future are related + _retEvt = Signal.new() :: Signal, Status>, + + _status = "initialized", + } :: Future, + { + __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 -- The constructed future +]=] +function Future.new(fn: (...any) -> T, args: { any }) + return _constructor(function(spawnEvt: Signal<()>, retEvt: Signal) + 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> -- The constructed future +]=] +function Future.try(fn: (...any) -> T, args: { any }) + return _constructor(function(spawnEvt: Signal<()>, retEvt: Signal, Status>) + spawnEvt:Fire() + + local ok, ret = pcall(fn, table.unpack(args)) + local res: Result = if ok then Ok(ret) else Err(ret) + retEvt:Fire(res, "ready") + end) +end + +--[=[ + @within Future + + Polls a [Future] to completion. + + @param self Future + @return (Status, Option) -- Returns the [Status] and an optional return if completed +]=] +function Future.poll(self: Future): (Status, Option) + 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 +]=] +function Future.cancel(self: Future) + 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 + @return T -- The value returned by the function on completion +]=] +function Future.await(self: Future): T + while true do + local status: Status, ret: Option = self:poll() + + if status == "ready" then + -- Safe to unwrap, we know it must not be nil + return ret:unwrap() + end + end +end + +return Future diff --git a/rbx/option.luau b/rbx/option.luau new file mode 100644 index 0000000..b284092 --- /dev/null +++ b/rbx/option.luau @@ -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 + if denominator == 0 then + None() + else + return Some(numerator / denominator) + end + end + ``` +]=] +local Option = {} + +export type Option = typeof(Option) & { + _optValue: T?, + typeId: "Option" +} + +--[=[ + @within Option + + No value. +]=] +function None(): Option + return Option.new(nil) :: Option +end + +--[=[ + @within Option + + Some value of type `T`. +]=] +function Some(val: T): Option + return Option.new(val) :: Option +end + +--[=[ + @within Option + + Converts a potentially `nil` value into an [Option]. + + @param val T? -- The value to convert into an [Option] + @return Option +]=] +function Option.new(val: T?) + return setmetatable( + { + _optValue = val, + typeId = "Option", + } :: Option, + { + __index = Option, + __tostring = function(self: Option) + if self._optValue == nil then + return `{self.typeId}::None` + else + return `{self.typeId}::Some({self._optValue})` + end + end, + + __eq = function(self: Option, other: Option): 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(self: Option, other: Option): boolean + if self:isSome() and other:isSome() then + return (self._optValue :: any) < (other._optValue :: any) + end + + return false + end, + __le = function(self: Option, other: Option): 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 + @return boolean +]=] +function Option.isSome(self: Option): 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 + @param op (val: T) -> boolean -- The predicate function + @return boolean +]=] +function Option.isSomeAnd(self: Option, 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 + @return boolean +]=] +function Option.isNone(self: Option): 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 = Some("value") + assert(x:expect("fruits are healthy") == "value") + ``` + + ```lua + local x: Option = None() + x:expect("fruits are healthy") -- panics with `fruits are healthy` + ``` + + @param self Option + @param msg string -- The panic message + @return boolean +]=] +function Option.expect(self: Option, 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 + @return T | never +]=] +function Option.unwrap(self: Option): 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 + @param default T -- The default value + @return T +]=] +function Option.unwrapOr(self: Option, 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 + @param default () -> T -- The function which computes the default value + @return T +]=] +function Option.unwrapOrElse(self: Option, default: () -> T): T + if self:isSome() then + return self._optValue :: T + end + + return default() +end + +--[=[ + @within Option + + Maps an [Option] to [Option] by applying a function to a contained value + (if [Option:Some]) or returns [Option:None](if [Option:None]). + + ```lua + local maybeSomeString: Option = Some("Hello, World!") + + local maybeSomeLen: Option = maybeSomeString:map(function(s) + return #s + end) + assert(maybeSomeLen == 13) + + local x: Option = None() + assert(x:map(function(s) + return #s + end) == None()) + ``` + + @param self Option + @param op (x: T) -> U? -- The function to apply + @return Option +]=] +function Option.map(self: Option, op: (x: T) -> U?): Option + 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 = Option.new(v[4]):inspect(function(x) + print("got: " .. x) + end) + + -- prints nothing + local x: Option = Option.new(v[5]):inspect(function(x) + print("got: " .. x) + end) + ``` + + @param self Option + @param op (x: T) -> () -- The function to call + @return Option +]=] +function Option.inspect(self: Option, op: (x: T) -> ()): Option + 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 = Some("foo") + assert(x.mapOr(42, function(v) return #v end) == 3) + + local x: Option = None() + assert(x.mapOr(42, function(v) return #v end) == 42) + ``` + @param self Option + @param default U -- The default value + @param op (val: T) -> U -- The function to apply + @return Option +]=] +function Option.mapOr(self: Option, 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 = Some("foo") + assert( + x:mapOrElse( + function() return 2 * k end, + function(v) return #v end + ) == 3 + ) + + local x: Option = None() + assert( + x:mapOrElse( + function() return 2 * k end, + function(v) return #v end + ) == 42 + ) + ``` + + @param self Option + @param default () -> U -- The function to compute the default value + @param op (val: T) -> U -- The function to apply + @return Option +]=] +function Option.mapOrElse(self: Option, 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 = Some(2) + local y: Option = None() + assert(x:and_(y) == None()) + + local x: Option = None() + local y: Option = Some("foo") + assert(x:and_(y) == None()) + + local x: Option = Some(2) + local y: Option = Some("foo") + assert(x:and_(y) == Some("foo")) + + local x: Option = None() + local y: Option = None() + assert(x:and_(y), None()) + ``` + + @param self Option + @param optb Option -- The other option + @return Option +]=] +function Option.and_(self: Option, optb: Option): Option + 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 + 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 = Option.new(arr2d[1]):andThen(function(row) + return row[2] + end) + assert(item01 == Some("A1")) + + local item20: Option = Option.new(arr2d[3]):andThen(function(row) + return row[0] + end) + assert(item20 == None()) + ``` + + @param self Option + @param op (val: T) -> Option -- The function to call + @return Option +]=] +function Option.andThen(self: Option, op: (val: T) -> Option): Option + 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 + @param predicate (val: T) -> boolean -- The predicate function which must match an element + @return Option +]=] +function Option.filter(self: Option, predicate: (val: T) -> boolean): Option + 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 = Some(2) + local y: Option = None() + assert(x:or_(y) == Some(2)) + + local x: Option = None() + local y: Option = Some(100) + assert(x:or_(y) == Some(100)) + + local x: Option = Some(2) + local y: Option = Some(100) + assert(x:or_(y) == Some(2)) + + local x: Option = None() + local y: Option = None() + assert(x:or_(y), None()) + ``` + + @param self Option + @param optb Option -- The other option + @return Option +]=] +function Option.or_(self: Option, optb: Option): Option + 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 + return None() + end + function vikings(): Option + 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 + @param op () -> Option -- The function to call + @return Option +]=] +function Option.orElse(self: Option, op: () -> Option): Option + 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 = Some(2) + local y: Option = None() + assert(x:xor(y) == Some(2)) + + local x: Option = None() + local y: Option = Some(2) + assert(x:xor(y) == Some(2)) + + local x: Option = Some(2) + local y: Option = Some(2) + assert(x:xor(y) == None()) + + local x: Option = None() + local y: Option = None() + assert(x:xor(y) == None()) + ``` + + @param self Option + @param optb Option -- The other option + @return Option +]=] +function Option.xor(self: Option, optb: Option): Option + 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 doesn’t update the value if the + option already contains [Option:Some]. + + ```lua + local opt: Option = 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 + @param val T -- The value to insert + @return T +]=] +function Option.insert(self: Option, 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 doesn’t update the value if the + option already contains [Option:Some]. + + ```lua + local opt: Option = 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 + @param val T -- The value to insert + @return T +]=] +function Option.getOrInsert(self: Option, 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 = Some(2) + local y: Option = x.take() + assert(x == None()) + assert(y == Some(2)) + + local x: Option = None() + local y: Option = x.take() + assert(x == None()) + assert(y == None()) + ``` + + @param self Option + @return Option +]=] +function Option.take(self: Option): Option + 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 = Some(2) + local old: Option = x:replace(5) + assert(x == Some(5)) + assert(old == Some(2)) + + local x: Option = None() + local old: Option = x:replace(3) + assert(x == Some(3)) + assert(old == None()) + ``` + + @param self Option + @param val T + @return Option +]=] +function Option.replace(self: Option, val: T): Option + local current: Option = 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 = Some(2) + local y: Option = None() + + assert(x:contains(2)) + assert(x:contains(4)) + assert(not y:contains(2)) + ``` + + @param self Option + @param val T + @return boolean +]=] +function Option.contains(self: Option, 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 = Some(1) + local y: Option = Some("hi") + local z: Option = None() + + assert(x:zip(y) == Some({ 1, "hi" })) + assert(x:zip(z) == None()) + ``` + + @param self Option + @param other Option>U> + @return Option<{T | U}> +]=] +function Option.zip(self: Option, other: Option): 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 = Some(17.5) + local yCoord: Option = 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 + @param other Option>U> + @param op (x: T, y: U) -> R? + @return Option +]=] +function Option.zipWith(self: Option, other: Option, op: (x: T, y: U) -> R?): Option + 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 + @return (Option, Option) +]=] +function Option.unzip(self: Option): (Option, Option) + 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 = Some("lol") + local y: Option = None() + + assert(x:unwrapUnchecked() == "lol") + assert(y:unwrapUnchecked() == nil) + ``` + + @param self Option + @return T? +]=] +function Option.unwrapUnchecked(self: Option): T? + return self._optValue +end + +--[=[ + @within Option + + Returns a formatted representation of the option, often + used for printing to stdout. + + ```lua + local x: Option = Some(123) + local y: Option = None() + + print(x:display()) -- prints `Option::Some(123)` + print(y:display()) -- prints `Option::None` + ``` + + @param self Option + @return string +]=] +function Option.display(self: Option): string + return tostring(self) +end + +return { + Option = Option, + Some = Some, + None = None, +} diff --git a/rbx/result.luau b/rbx/result.luau new file mode 100644 index 0000000..621dc0a --- /dev/null +++ b/rbx/result.luau @@ -0,0 +1,900 @@ +local tableEq = require(script.Parent:WaitForChild('util')).tableEq + +--[=[ + @class Result + + Error handling with the [Result] type. + + [Result]`` 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 + + -- 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 + 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 = Ok(10) + local badResult: Result = 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 = goodResult:andThen(function(i) return Ok(i == 11) end) + + -- Use `or_else` to handle the error + local badResult: Result = 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 = typeof(Result) & { + _value: T, + _error: E, + typeId: "Result" +} + +--[=[ + @within Result + + Contains the success value. + + @param val T + @return Result +]=] +function Ok(val: T): Result + return Result.new(val, (nil :: unknown) :: E) +end + +--[=[ + @within Result + + Contains the error value. + + @param err E + @return Result +]=] +function Err(err: E): Result + 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 +]=] +function Result.new(val: T, err: E) + return setmetatable( + { + _value = val, + _error = err, + typeId = "Result", + } :: Result, + { + __index = Result, + __tostring = function(self: Result) + 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}` + end, + __eq = function(self: Result, other: Result) + 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 = Ok(3) + assert(x:isOk()) + + x = Err("Some error message") + assert(not x:isOk()) + ``` + + @param self Result + @return boolean +]=] +function Result.isOk(self: Result): 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 = 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 + @param predicate (val: T?) -> boolean + @return boolean +]=] +function Result.isOkAnd(self: Result, 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 = Ok(3) + assert(not x:isErr()) + + x = Err("Some error message") + assert(x:isErr()) + ``` + + @param self Result + @return boolean +]=] +function Result.isErr(self: Result) + 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 = 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 + @param predicate (val: T?) -> boolean + @return boolean +]=] +function Result.isErrAnd(self: Result, predicate: (val: E) -> boolean): boolean + if self:isErr() then + return predicate(self._error) + end + + return false +end + +--[=[ + @within Result + + Maps a [Result] to [Result] 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 + 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 + @param op (val: T) -> U + @return Result +]=] +function Result.map(self: Result, op: (val: T) -> U): Result + if self:isOk() then + return Result.new(op(self._value), self._error) :: Result + 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 = Ok("foo") + assert(x:mapOr("bar", string.upper) == "FOO") + + x = Err("foo") + assert(x:mapOr("bar", string.upper) == "bar") + ``` + + @param self Result + @param default U + @param op (val: T) -> U + @return U +]=] +function Result.mapOr(self: Result, default: U, op: (val: T) -> U): U + if self:isOk() then + return op(self._value) + end + + return default +end + +--[=[ + @within Result + + Maps a [Result] 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 = 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 + @param default (val: E) -> U + @param op (val: T) -> U + @return U +]=] +function Result.mapOrElse(self: Result, 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] to [Result] 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 = Ok(2) + assert(x:mapErr(stringify) == Ok(2)) + + x = Err(13) + assert(x:mapErr(stringify) == Err("error code: 13")) + ``` + + @param self Result + @param op (val: E) -> F + @return Result +]=] +function Result.mapErr(self: Result, op: (val: E) -> F): Result + 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 + 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 + @param op (val: E) -> () + @return Result +]=] +function Result.inspect(self: Result, op: (val: T) -> ()): Result + 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 + 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 + @param op (val: E) -> () + @return Result +]=] +function Result.inspectErr(self: Result, op: (val: E) -> ()): Result + if self:isErr() then + op(self._error) + end + + return self +end + +-- TODO: Iterator traits +-- selene: allow(unused_variable) +function Result.iter(self: Result): 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 = 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(var: string): Result + 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 you’re 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 + @param msg string + @return T | never +]=] +function Result.expect(self: Result, 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 = Ok(2) + assert(x:unwrap() == 2) + + x = Err("oh no") + x:unwrap() -- panics with `oh no` + ``` + + @param self Result +]=] +function Result.unwrap(self: Result): 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(self: Result): 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 = Ok(10) + x:expectErr("Testing expect") -- panics with `Testing expect: 10` + ``` + + @param self Result + @param msg string + @return E | never +]=] +function Result.expectErr(self: Result, 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 = Ok(2) + x:unwrapErr() -- panics with `2` + ``` + ```lua + x = Err("oh no") + assert(x:unwrapErr() == "oh no") + ``` + + @param self Result + @return E | never +]=] +function Result.unwrapErr(self: Result): 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(self: Result): never + return error("Unimplemented: `Result:intoOk()`") +end +-- selene: allow(unused_variable) +function Result.intoErr(self: Result): 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 = Ok(2) + local y: Result = Err("late error") + assert(x:and_(y) == Err("late error")) + + local x: Result = Err("early error") + local y: Result = Ok("foo") + assert(x:and_(y) == Err("early error")) + + local x: Result = Err("not a 2") + local y: Result = Err("late error") + assert(x:and_(y) == Err("not a 2")) + + local x: Result = Ok(2) + local y: Result = Ok("different result type") + assert(x:and_(y) == Ok("different result type")) + ``` + + @param self Result + @param res Result + @return Result +]=] +function Result.and_(self: Result, res: Result): Result + 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 + 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 + @param op (...any) -> any + @return Result +]=] +function Result.andThen(self: Result, op: (...any) -> any): Result + if self:isOk() then + return op(self._value) + end + + return Err(self._error) :: Result +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 = Ok(2) + local y: Result = Err("late error") + assert(x:or_(y) == Ok(2)) + + local x: Result = Err("early error") + local y: Result = Ok(2) + assert(x:or_(y) == Ok(2)) + + local x: Result = Err("not a 2") + local y: Result = Err("late error") + assert(x:or_(y) == Err("late error")) + + local x: Result = Ok(2) + local y: Result = Ok(100) + assert(x:or_(y) == Ok(2)) + ``` + + @param self Result + @param res Result + @return Result | never +]=] +function Result.or_(self: Result, res: Result): Result | never + if self:isErr() then + return res + end + + if self:isOk() then + return Ok(self._value) :: Result + 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 + return Ok(x * x) + end + + function err(x: number): Result + 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 + @param op (x: E) -> Result + @return Result +]=] +function Result.orElse(self: Result, op: (x: E) -> Result): Result + 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 = Ok(9) + assert(x:unwrapOr(default), 9) + + x = Err("error") + assert(x:unwrapOr(default), default) + ``` + + @param self Result + @param default T + @return T +]=] +function Result.unwrapOr(self: Result, 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 + @param op (x: E) -> T + @return T +]=] +function Result.unwrapOrElse(self: Result, 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 = 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 + @param val T + @return boolean +]=] +function Result.contains(self: Result, 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 = 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 + @param err E + @return boolean +]=] +function Result.containsErr(self: Result, 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 = Ok(123) + local y: Result = Err("error") + + print(x:display()) -- prints `Result::Ok(123)` + print(y:display()) -- prints `Result::Err("error")` + ``` + + @param self Result + @return string +]=] +function Result.display(self: Result): string + return tostring(self) +end + +return { + Ok = Ok, + Err = Err, + Result = Result, +} diff --git a/rbx/util.luau b/rbx/util.luau new file mode 100644 index 0000000..743e413 --- /dev/null +++ b/rbx/util.luau @@ -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, +} diff --git a/rusty-luau.luau b/rusty-luau.luau new file mode 100644 index 0000000..e69de29 diff --git a/sourcemap.json b/sourcemap.json new file mode 100644 index 0000000..a90d4dd --- /dev/null +++ b/sourcemap.json @@ -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"]}]} \ No newline at end of file