From fb97755cad68d8fd0114a128d284767b65b3db73 Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Fri, 26 Apr 2024 10:55:22 +0530 Subject: [PATCH] docs + refactor: document all of `Result` module & minor refactors/fixes * Include moonwave doc comments for `Result` * Renamed `Option:getInner` to `Option:unwrapUnchecked` * Updated both `Option.__eq` and `Result.__eq` to support tables * Replaced nil returns in types with unit type * Make unimplemented methods throw an error * Rename `Result.exceptErr` to `Result.expectErr` * Add missing return type in `Result.unwrapOrElse` --- lib/option.luau | 41 +-- lib/result.luau | 672 +++++++++++++++++++++++++++++++++++++++++++++++- lib/util.luau | 38 +++ 3 files changed, 705 insertions(+), 46 deletions(-) create mode 100644 lib/util.luau diff --git a/lib/option.luau b/lib/option.luau index 12c3145..fa4f422 100644 --- a/lib/option.luau +++ b/lib/option.luau @@ -1,37 +1,4 @@ --- 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 +local tableEq = require("util").tableEq --[=[ @class Option @@ -911,14 +878,14 @@ end local x: Option = Some("lol") local y: Option = None() - assert(x:getInner() == "lol") - assert(y:getInner() == nil) + assert(x:unwrapUnchecked() == "lol") + assert(y:unwrapUnchecked() == nil) ``` @param self Option @return T? ]=] -function Option.getInner(self: Option): T? +function Option.unwrapUnchecked(self: Option): T? return self._optValue end diff --git a/lib/result.luau b/lib/result.luau index d59beab..4a67700 100644 --- a/lib/result.luau +++ b/lib/result.luau @@ -1,18 +1,111 @@ +local tableEq = require("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( { @@ -43,7 +136,7 @@ function Result.new(val: T, err: E) return self._value == other._value and self._error == other._error end - error(`eq: cannot compare {tostring(self)} and {tostring(other)}`) + return tableEq(self._value, other._value) and tableEq(self._error, other._error) end, -- TODO: Implement equality and arithmetic metamethods @@ -52,6 +145,22 @@ function Result.new(val: T, err: E) ) 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 @@ -60,14 +169,72 @@ function Result.isOk(self: Result): boolean 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) @@ -76,6 +243,42 @@ function Result.isErrAnd(self: Result, predicate: (val: E) -> boolea 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 @@ -84,6 +287,29 @@ function Result.map(self: Result, op: (val: T) -> U): 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) @@ -92,6 +318,27 @@ function Result.mapOr(self: Result, default: U, op: (val: T) -> U 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) @@ -100,6 +347,31 @@ function Result.mapOrElse(self: Result, default: (val: E) -> U, o 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)) @@ -108,7 +380,35 @@ function Result.mapErr(self: Result, op: (val: E) -> F): Result(self: Result, op: (val: T) -> nil): Result +--[=[ + @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 @@ -116,7 +416,35 @@ function Result.inspect(self: Result, op: (val: T) -> nil): Result(self: Result, op: (val: E) -> nil): Result +--[=[ + @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 @@ -125,8 +453,56 @@ function Result.inspectErr(self: Result, op: (val: E) -> nil): Resul end -- TODO: Iterator traits -function Result.iter() end +function Result.iter(): 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 @@ -135,6 +511,26 @@ function Result.expect(self: Result, msg: string): T | never 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 @@ -143,11 +539,28 @@ function Result.unwrap(self: Result): T | never return error(`panic: \`Result:unwrap()\` called on an \`Err\` value: {self._error}`) end +-- TODO: default values for types function Result.unwrapOrDefault(self: Result): never - return error("TODO: Result:unwrapOrDefault()") + return error("Unimplemented: `Result:unwrapOrDefault()`") end -function Result.exceptErr(self: Result, msg: string): E | never +--[=[ + @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 @@ -155,6 +568,25 @@ function Result.exceptErr(self: Result, msg: string): E | never 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 @@ -164,9 +596,41 @@ function Result.unwrapErr(self: Result): E | never end -- TODO: How the fuck do I implement this? -function Result.intoOk(self: Result) end -function Result.intoErr(self: Result) end +function Result.intoOk(self: Result): never + return error("Unimplemented: `Result:intoOk()`") +end +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 @@ -175,6 +639,36 @@ function Result.and_(self: Result, res: Result): 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) @@ -183,6 +677,37 @@ function Result.andThen(self: Result, op: (...any) -> any): Resul 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 @@ -195,6 +720,34 @@ function Result.or_(self: Result, res: Result): 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) @@ -203,6 +756,28 @@ function Result.orElse(self: Result, op: (x: E) -> Result): 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 @@ -211,7 +786,25 @@ function Result.unwrapOr(self: Result, default: T): T return default end -function Result.unwrapOrElse(self: Result, op: (x: E) -> T) +--[=[ + @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 @@ -219,6 +812,26 @@ function Result.unwrapOrElse(self: Result, op: (x: E) -> T) 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 @@ -227,6 +840,26 @@ function Result.contains(self: Result, val: T): boolean 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 @@ -235,6 +868,27 @@ function Result.containsErr(self: Result, err: E): boolean 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, diff --git a/lib/util.luau b/lib/util.luau new file mode 100644 index 0000000..743e413 --- /dev/null +++ b/lib/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, +}