feat: finalize Option & Result implementations

This commit is contained in:
Erica Marigold 2024-04-01 16:00:55 +05:30
parent 9d78c7edec
commit ef451dace6
No known key found for this signature in database
GPG key ID: 2768CC0C23D245D1
3 changed files with 315 additions and 49 deletions

97
lib/conversion.luau Normal file
View file

@ -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<T> = option.Option<T>
local None = option.None
local Some = option.Some
local result = require("result")
local Result = result.Result
export type Result<T, E> = result.Result<T, E>
local Ok = result.Ok
local Err = result.Err
function Result.ok<T, E>(self: Result<T, E>): Option<T>
if self:isOk() then
if self._value == nil then
return None()
end
return Some(self._value)
end
return None()
end
function Result.err<T, E>(self: Result<T, E>): Option<E>
if self:isErr() then
return Option.new(self._error) :: Option<E>
end
return None()
end
function Result.transpose<T, E>(self: Result<Option<T>, E>): Option<Result<T, E>>
-- 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<T, E>(self: Option<T>, err: E): Result<T, E>
if self:isSome() then
return Ok(self._optValue)
end
return Err(err)
end
function Option.okOrElse<T, E>(self: Option<T>, err: () -> E): Result<T, E>
if self:isSome() then
return Ok(self._optValue :: T)
end
return Err(err())
end
function Option.transpose<T, E>(self: Option<Result<T, E>>): Result<Option<T>, E>
if self:isSome() then
local inner = self._optValue
assert(self.typeId == "Option" and inner.typeId == "Result", "Only an `Option` of a `Result` can be transposed")
if inner:isOk() then
return Some(Ok(inner._value))
elseif inner:isErr() then
return Some(Err(inner._error))
end
end
return Ok(None())
end
return {
Ok = Ok,
Err = Err,
Result = Result,
Some = Some,
None = None,
Option = Option,
}

View file

@ -30,11 +30,227 @@ function Option.new<T>(val: T?)
end end
end, end,
} }
-- TODO: Implement equality and arithmetic metamethods
-- TODO: Implement __iter, once iterators traits exist
) )
end end
function Option.isSome<T>(self: Option<T>): boolean
return self._optValue ~= nil
end
function Option.isSomeAnd<T>(self: Option<T>, op: (val: T) -> boolean): boolean
-- We know that it's not None, so this is fine
return self:isSome() and op(self._optValue :: T)
end
function Option.isNone<T>(self: Option<T>): boolean
return not self:isSome()
end
function Option.expect<T>(self: Option<T>, msg: string): T | never
if self:isSome() then
return self._optValue :: T
end
return error(`panic: {msg}; expected Option to be type Option::Some(T), got Option::None`)
end
function Option.unwrap<T>(self: Option<T>): T | never
if self:isSome() then
return self._optValue :: T
end
return error("called `Option:unwrap()` on a `None` value")
end
function Option.unwrapOr<T>(self: Option<T>, default: T): T
if self:isSome() then
return self._optValue :: T
end
return default
end
function Option.unwrapOrElse<T>(self: Option<T>, default: () -> T): T
if self:isSome() then
return self._optValue :: T
end
return default()
end
function Option.map<T, U>(self: Option<T>, op: (x: T) -> U?): Option<U>
if self:isSome() then
local val = op(self._optValue :: T)
if val == nil then
return None()
end
return Some(val)
end
return None()
end
function Option.inspect<T>(self: Option<T>, op: (x: T) -> nil): Option<T>
if self:isSome() then
op(self._optValue :: T)
end
return self
end
function Option.mapOr<T, U>(self: Option<T>, default: U, op: (val: T) -> U)
if self:isSome() then
return op(self._optValue :: T)
end
return default
end
function Option.mapOrElse<T, U>(self: Option<T>, default: () -> U, op: (val: T) -> U): U
if self:isSome() then
return op(self._optValue :: T)
end
return default()
end
-- TODO: Iterator traits
function Option.iter() end
function Option.and_<T, U>(self: Option<T>, optb: Option<U>): Option<U>
if self:isSome() then
return optb
end
return None()
end
function Option.andThen<T, U>(self: Option<T>, op: (val: T) -> Option<U>): Option<U>
if self:isSome() then
return op(self._optValue :: T)
end
return None()
end
function Option.filter<T>(self: Option<T>, predicate: (val: T) -> boolean): Option<T>
if self:isSome() then
if predicate(self._optValue :: T) then
return self
end
end
return None()
end
function Option.or_<T>(self: Option<T>, optb: Option<T>): Option<T>
if self:isSome() then
return self
end
return optb
end
function Option.orElse<T>(self: Option<T>, op: () -> Option<T>): Option<T>
if self:isSome() then
return self
end
return op()
end
function Option.xor<T>(self: Option<T>, optb: Option<T>): Option<T>
if self:isSome() and optb:isSome() then
return None()
end
if self:isSome() then
return self
elseif optb:isSome() then
return optb
end
return None()
end
function Option.insert<T>(self: Option<T>, val: T): T
self._optValue = val
return self._optValue :: T
end
function Option.getOrInsert<T>(self: Option<T>, val: T): T
if self:isNone() then
self._optValue = val
end
return self._optValue :: T
end
function Option.take<T>(self: Option<T>): Option<T>
if self:isSome() then
local val = self._optValue :: T
self._optValue = nil
return Some(val)
end
return None()
end
function Option.replace<T>(self: Option<T>, val: T): Option<T>
local current: Option<T> = self
self._optValue = val
if current:isNone() then
return current
end
return Some(current._optValue :: T)
end
function Option.contains<T>(self: Option<T>, val: T): boolean
if self:isSome() then
return self._optValue == val
end
return false
end
function Option.zip<T, U>(self: Option<T>, other: Option<U>): Option<{ T | U }>
if self:isSome() and other:isSome() then
return Some({ self._optValue, other._optValue })
end
return None()
end
function Option.zipWith<T, U, R>(self: Option<T>, other: Option<U>, op: (x: T, y: U) -> R?): Option<R>
if self:isSome() and other:isSome() then
local computed = op(self._optValue :: T, other._optValue :: U)
if computed ~= nil then
return Some(computed :: R)
end
end
return None()
end
function Option.unzip<T, A, B>(self: Option<T>): (Option<A>, Option<B>)
if self:isSome() then
if self:isSome() and typeof(self._optValue) == "table" and #self._optValue == 2 then
return Some(self._optValue[1] :: A), Some(self._optValue[2] :: B)
end
end
return None(), None()
end
return { return {
-- TODO: Implement Option utility methods
Option = Option, Option = Option,
Some = Some, Some = Some,
None = None, None = None,

View file

@ -1,9 +1,3 @@
local option = require("option")
local Option = option.Option
type Option<T> = option.Option<T>
local None = option.None
local Some = option.Some
local Result = {} local Result = {}
export type Result<T, E> = typeof(Result) & { export type Result<T, E> = typeof(Result) & {
_value: T, _value: T,
@ -52,6 +46,7 @@ function Result.new<T, E>(val: T, err: E)
error(`eq: cannot compare {tostring(self)} and {tostring(other)}`) error(`eq: cannot compare {tostring(self)} and {tostring(other)}`)
end, end,
-- TODO: Implement equality and arithmetic metamethods
-- TODO: Implement __iter, once iterators traits exist -- TODO: Implement __iter, once iterators traits exist
} }
) )
@ -81,26 +76,6 @@ function Result.isErrAnd<T, E>(self: Result<T, E>, predicate: (val: E) -> boolea
return false return false
end end
function Result.ok<T, E>(self: Result<T, E>): Option<T>
if self:isOk() then
if self._value == nil then
return None()
end
return Some(self._value)
end
return None()
end
function Result.err<T, E>(self: Result<T, E>): Option<E>
if self:isErr() then
return Option.new(self._error) :: Option<E>
end
return None()
end
function Result.map<T, E, U>(self: Result<T, E>, op: (val: T) -> U): Result<U, E> function Result.map<T, E, U>(self: Result<T, E>, op: (val: T) -> U): Result<U, E>
if self:isOk() then if self:isOk() then
return Result.new(op(self._value), self._error) :: Result<U, E> return Result.new(op(self._value), self._error) :: Result<U, E>
@ -260,30 +235,8 @@ function Result.containsErr<T, E>(self: Result<T, E>, err: E): boolean
return false return false
end end
function Result.transpose<T, E>(self: Result<Option<T>, E>): Option<Result<T, E>>
-- 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<Option<string>, string> = Ok(None())
print(tostring(x))
return { return {
Ok = Ok, Ok = Ok,
Err = Err, Err = Err,
Result = Result, Result = Result,
} }
-- print(y:transpose()) -- this should have a typeerror, i need to fix this