feat: initial Result and barebones Option implementation

This commit is contained in:
Erica Marigold 2024-04-01 11:37:43 +05:30
commit 538fd90b0c
No known key found for this signature in database
GPG key ID: 2768CC0C23D245D1
4 changed files with 329 additions and 0 deletions

13
.vscode/settings.json vendored Normal file
View file

@ -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
}

5
aftman.toml Normal file
View file

@ -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"

34
lib/option.luau Normal file
View file

@ -0,0 +1,34 @@
local Option = {}
export type Option<T> = typeof(Option) & {
_optValue: T?,
typeId: "Option"
}
function None<T>(): Option<T>
return Option.new(nil) :: Option<T>
end
function Some<T>(val: T): Option<T>
return Option.new(val) :: Option<T>
end
function Option.new<T>(val: T?)
return setmetatable({
_optValue = val,
typeId = "Option"
} :: Option<T>, {
__index = Option,
__tostring = function(self)
-- Return formatted enum variants too
return `{self.typeId}<T>`
end
})
end
return {
-- TODO: Implement Option utility methods
Option = Option,
Some = Some,
None = None
}

277
lib/result.luau Normal file
View file

@ -0,0 +1,277 @@
local option = require("option")
local Option = option.Option
type Option<T> = option.Option<T>
local None = option.None
local Some = option.Some
local Result = {}
export type Result<T, E> = typeof(Result) & {
_value: T,
_error: E,
typeId: "Result",
}
function Ok<T, E>(val: T): Result<T, E>
return Result.new(val, (nil :: unknown) :: E)
end
function Err<T, E>(err: E): Result<T, E>
return Result.new((nil :: unknown) :: T, err)
end
function Result.new<T, E>(val: T, err: E)
return setmetatable(
{
_value = val,
_error = err,
typeId = "Result",
} :: Result<T, E>,
{
__index = Result,
__tostring = function<T, E>(self: Result<T, E>)
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}}<T, E>`
end,
__eq = function<T, E>(self: Result<T, E>, other: Result<T, E>)
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<T, E>(self: Result<T, E>): boolean
if self._value == nil then
return false
end
return true
end
function Result.isOkAnd<T, E>(self: Result<T, E>, predicate: (val: T?) -> boolean): boolean
return self:isOk() and predicate(self._value)
end
function Result.isErr<T, E>(self: Result<T, E>)
return not self:isOk()
end
function Result.isErrAnd<T, E>(self: Result<T, E>, predicate: (val: E) -> boolean): boolean
if self:isErr() then
return predicate(self._error)
end
return false
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>
if self:isOk() then
return Result.new(op(self._value), self._error) :: Result<U, E>
end
return Err(self._error)
end
function Result.mapOr<T, E, U>(self: Result<T, E>, default: U, op: (val: T) -> U): U
if self:isOk() then
return op(self._value)
end
return default
end
function Result.mapOrElse<T, E, U>(self: Result<T, E>, 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<T, E, F>(self: Result<T, E>, op: (val: E) -> F): Result<T, F>
if self:isErr() then
return Result.new(self._value, op(self._error))
end
return Ok(self._value)
end
function Result.inspect<T, E>(self: Result<T, E>, op: (val: T) -> nil): Result<T, E>
if self:isOk() then
op(self._value)
end
return self
end
function Result.inspectErr<T, E>(self: Result<T, E>, op: (val: E) -> nil): Result<T, E>
if self:isErr() then
op(self._error)
end
return self
end
-- TODO: Iterator traits
function Result.iter() end
function Result.expect<T, E>(self: Result<T, E>, msg: string): T | never
if self:isOk() then
return self._value
end
return error(`panic: {msg}; {self._error}`)
end
function Result.unwrap<T, E>(self: Result<T, E>): 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<T, E>(self: Result<T, E>): never
return error("TODO: Result:unwrapOrDefault()")
end
function Result.exceptErr<T, E>(self: Result<T, E>, msg: string): E | never
if self:isErr() then
return self._error
end
return error(`panic: {msg}; {self._error}`)
end
function Result.unwrapErr<T, E>(self: Result<T, E>): 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<T, E>(self: Result<T, E>) end
function Result.intoErr<T, E>(self: Result<T, E>) end
function Result.and_<T, E, U>(self: Result<T, E>, res: Result<U, E>): Result<U, E>
if self:isOk() then
return res
end
return Err(self._error)
end
function Result.andThen<T, E, U>(self: Result<T, E>, op: (...any) -> any): Result<U, E>
if self:isOk() then
return op(self._value)
end
return Err(self._error) :: Result<U, E>
end
function Result.or_<T, E, F>(self: Result<T, E>, res: Result<T, F>): Result<T, F> | never
if self:isErr() then
return res
end
if self:isOk() then
return Ok(self._value) :: Result<T, F>
end
return error("called `Result:or()` with an invalid value")
end
function Result.orElse<T, E, U>(self: Result<T, E>, op: (x: E) -> Result<T, U>): Result<T, U>
if self:isErr() then
return op(self._error)
end
return Ok(self._value)
end
function Result.unwrapOr<T, E>(self: Result<T, E>, default: T): T
if self:isOk() then
return self._value
end
return default
end
function Result.unwrapOrElse<T, E>(self: Result<T, E>, op: (x: E) -> T)
if self:isOk() then
return self._value
end
return op(self._error)
end
function Result.contains<T, E>(self: Result<T, E>, val: T): boolean
if self:isOk() then
return self._value == val
end
return false
end
function Result.containsErr<T, E>(self: Result<T, E>, err: E): boolean
if self:isErr() then
return self._error == err
end
return false
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) ~= nil then
return Some(Ok(self._value._optValue))
elseif self:isErr() then
return Some(Err(self._error))
end
error("`Result` is not transposable")
end