From ef451dace696bd5d437c71f4ff08c7ffa1a7aebb Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Mon, 1 Apr 2024 16:00:55 +0530 Subject: [PATCH] feat: finalize `Option` & `Result` implementations --- lib/conversion.luau | 97 ++++++++++++++++++++ lib/option.luau | 218 +++++++++++++++++++++++++++++++++++++++++++- lib/result.luau | 49 +--------- 3 files changed, 315 insertions(+), 49 deletions(-) create mode 100644 lib/conversion.luau diff --git a/lib/conversion.luau b/lib/conversion.luau new file mode 100644 index 0000000..e77e38e --- /dev/null +++ b/lib/conversion.luau @@ -0,0 +1,97 @@ +--[[ + 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("option") +local Option = option.Option +export type Option = option.Option +local None = option.None +local Some = option.Some + +local result = require("result") +local Result = result.Result +export type Result = result.Result +local Ok = result.Ok +local Err = result.Err + +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.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) then + return Some(Ok(self._value._optValue)) + elseif self:isErr() then + return Some(Err(self._error)) + end + + error("`Result` is not transposable") +end + +function Option.okOr(self: Option, err: E): Result + if self:isSome() then + return Ok(self._optValue) + end + + return Err(err) +end + +function Option.okOrElse(self: Option, err: () -> E): Result + if self:isSome() then + return Ok(self._optValue :: T) + end + + return Err(err()) +end + +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/lib/option.luau b/lib/option.luau index 13e4828..850ed96 100644 --- a/lib/option.luau +++ b/lib/option.luau @@ -30,11 +30,227 @@ function Option.new(val: T?) end end, } + + -- TODO: Implement equality and arithmetic metamethods + -- TODO: Implement __iter, once iterators traits exist ) end +function Option.isSome(self: Option): boolean + return self._optValue ~= nil +end + +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 + +function Option.isNone(self: Option): boolean + return not self:isSome() +end + +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 + +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 + +function Option.unwrapOr(self: Option, default: T): T + if self:isSome() then + return self._optValue :: T + end + + return default +end + +function Option.unwrapOrElse(self: Option, default: () -> T): T + if self:isSome() then + return self._optValue :: T + end + + return default() +end + +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 + +function Option.inspect(self: Option, op: (x: T) -> nil): Option + if self:isSome() then + op(self._optValue :: T) + end + + return self +end + +function Option.mapOr(self: Option, default: U, op: (val: T) -> U) + if self:isSome() then + return op(self._optValue :: T) + end + + return default +end + +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() end + +function Option.and_(self: Option, optb: Option): Option + if self:isSome() then + return optb + end + + return None() +end + +function Option.andThen(self: Option, op: (val: T) -> Option): Option + if self:isSome() then + return op(self._optValue :: T) + end + + return None() +end + +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 + +function Option.or_(self: Option, optb: Option): Option + if self:isSome() then + return self + end + + return optb +end + +function Option.orElse(self: Option, op: () -> Option): Option + if self:isSome() then + return self + end + + return op() +end + +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 + +function Option.insert(self: Option, val: T): T + self._optValue = val + return self._optValue :: T +end + +function Option.getOrInsert(self: Option, val: T): T + if self:isNone() then + self._optValue = val + end + + return self._optValue :: T +end + +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 + +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 + +function Option.contains(self: Option, val: T): boolean + if self:isSome() then + return self._optValue == val + end + + return false +end + +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 + +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 + +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 + return { - -- TODO: Implement Option utility methods Option = Option, Some = Some, None = None, diff --git a/lib/result.luau b/lib/result.luau index 8a89be0..d59beab 100644 --- a/lib/result.luau +++ b/lib/result.luau @@ -1,9 +1,3 @@ -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, @@ -52,6 +46,7 @@ function Result.new(val: T, err: E) error(`eq: cannot compare {tostring(self)} and {tostring(other)}`) end, + -- TODO: Implement equality and arithmetic metamethods -- TODO: Implement __iter, once iterators traits exist } ) @@ -81,26 +76,6 @@ function Result.isErrAnd(self: Result, predicate: (val: E) -> boolea 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 @@ -260,30 +235,8 @@ function Result.containsErr(self: Result, err: E): boolean 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) then - return Some(Ok(self._value._optValue)) - elseif self:isErr() then - return Some(Err(self._error)) - end - - error("`Result` is not transposable") -end - -local x: Result, string> = Ok(None()) - -print(tostring(x)) - return { Ok = Ok, Err = Err, Result = Result, } - --- print(y:transpose()) -- this should have a typeerror, i need to fix this