From 538fd90b0cadee4ba62a7980ff06d73369dd7b6e Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Mon, 1 Apr 2024 11:37:43 +0530 Subject: [PATCH] feat: initial Result and barebones Option implementation --- .vscode/settings.json | 13 ++ aftman.toml | 5 + lib/option.luau | 34 ++++++ lib/result.luau | 277 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 329 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 aftman.toml create mode 100644 lib/option.luau create mode 100644 lib/result.luau diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..167d616 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "luau-lsp.inlayHints.variableTypes": true, + "luau-lsp.inlayHints.functionReturnTypes": true, + "luau-lsp.inlayHints.parameterNames": "all", + "luau-lsp.inlayHints.parameterTypes": true, + "luau-lsp.inlayHints.typeHintMaxLength": 50, + "luau-lsp.require.mode": "relativeToFile", + "luau-lsp.require.directoryAliases": { + "@lune/": "~/.lune/.typedefs/0.8.2/" + }, + + "editor.formatOnSave": true +} diff --git a/aftman.toml b/aftman.toml new file mode 100644 index 0000000..208aba1 --- /dev/null +++ b/aftman.toml @@ -0,0 +1,5 @@ +[tools] +lune = "lune-org/lune@0.8.2" +stylua = "JohnnyMorganz/StyLua@0.20.0" +luau-lsp = "JohnnyMorganz/luau-lsp@1.27.0" +darklua = "seaofvoices/darklua@0.13.0" diff --git a/lib/option.luau b/lib/option.luau new file mode 100644 index 0000000..5d7fce6 --- /dev/null +++ b/lib/option.luau @@ -0,0 +1,34 @@ +local Option = {} +export type Option = typeof(Option) & { + _optValue: T?, + typeId: "Option" +} + +function None(): Option + return Option.new(nil) :: Option +end + +function Some(val: T): Option + return Option.new(val) :: Option +end + +function Option.new(val: T?) + return setmetatable({ + _optValue = val, + typeId = "Option" + } :: Option, { + __index = Option, + __tostring = function(self) + -- Return formatted enum variants too + + return `{self.typeId}` + end + }) +end + +return { + -- TODO: Implement Option utility methods + Option = Option, + Some = Some, + None = None +} \ No newline at end of file diff --git a/lib/result.luau b/lib/result.luau new file mode 100644 index 0000000..8be4961 --- /dev/null +++ b/lib/result.luau @@ -0,0 +1,277 @@ +local option = require("option") +local Option = option.Option +type Option = option.Option +local None = option.None +local Some = option.Some + +local Result = {} +export type Result = typeof(Result) & { + _value: T, + _error: E, + typeId: "Result", +} + +function Ok(val: T): Result + return Result.new(val, (nil :: unknown) :: E) +end + +function Err(err: E): Result + return Result.new((nil :: unknown) :: T, err) +end + +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 + + error(`eq: cannot compare {tostring(self)} and {tostring(other)}`) + end, + + -- TODO: Implement __iter, once iterators traits exist + } + ) +end + +function Result.isOk(self: Result): boolean + if self._value == nil then + return false + end + + return true +end + +function Result.isOkAnd(self: Result, predicate: (val: T?) -> boolean): boolean + return self:isOk() and predicate(self._value) +end + +function Result.isErr(self: Result) + return not self:isOk() +end + +function Result.isErrAnd(self: Result, predicate: (val: E) -> boolean): boolean + if self:isErr() then + return predicate(self._error) + end + + return false +end + +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 + +function Result.err(self: Result): Option + if self:isErr() then + return Option.new(self._error) :: Option + end + + return None() +end + +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 + +function Result.mapOr(self: Result, default: U, op: (val: T) -> U): U + if self:isOk() then + return op(self._value) + end + + return default +end + +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 + +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 + +function Result.inspect(self: Result, op: (val: T) -> nil): Result + if self:isOk() then + op(self._value) + end + + return self +end + +function Result.inspectErr(self: Result, op: (val: E) -> nil): Result + if self:isErr() then + op(self._error) + end + + return self +end + +-- TODO: Iterator traits +function Result.iter() end + +function Result.expect(self: Result, msg: string): T | never + if self:isOk() then + return self._value + end + + return error(`panic: {msg}; {self._error}`) +end + +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 + +function Result.unwrapOrDefault(self: Result): never + return error("TODO: Result:unwrapOrDefault()") +end + +function Result.exceptErr(self: Result, msg: string): E | never + if self:isErr() then + return self._error + end + + return error(`panic: {msg}; {self._error}`) +end + +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? +function Result.intoOk(self: Result) end +function Result.intoErr(self: Result) end + +function Result.and_(self: Result, res: Result): Result + if self:isOk() then + return res + end + + return Err(self._error) +end + +function Result.andThen(self: Result, op: (...any) -> any): Result + if self:isOk() then + return op(self._value) + end + + return Err(self._error) :: Result +end + +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 + +function Result.orElse(self: Result, op: (x: E) -> Result): Result + if self:isErr() then + return op(self._error) + end + + return Ok(self._value) +end + +function Result.unwrapOr(self: Result, default: T): T + if self:isOk() then + return self._value + end + + return default +end + +function Result.unwrapOrElse(self: Result, op: (x: E) -> T) + if self:isOk() then + return self._value + end + + return op(self._error) +end + +function Result.contains(self: Result, val: T): boolean + if self:isOk() then + return self._value == val + end + + return false +end + +function Result.containsErr(self: Result, err: E): boolean + if self:isErr() then + return self._error == err + end + + return false +end + +function Result.transpose(self: Result, E>): Option> + -- TODO: Instead of checking whether values are nil, use + -- utility methods for Options once available + if self._value == None() then + return None() + elseif self:isOkAnd(function(val): boolean + return val._optValue == nil + end) ~= nil then + return Some(Ok(self._value._optValue)) + elseif self:isErr() then + return Some(Err(self._error)) + end + + error("`Result` is not transposable") +end