-- 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 --[=[ @class Option Type [Option] represents an optional value: every [Option] is either [Some] and contains a value, or [None], and does not. Common uses of an [Option] may involve: * Initial values * Return values for functions that are not defined over their entire input range (partial functions) * Return value for otherwise reporting simple errors, where None is returned on error * Optional object fields * Values that can be loaned or “taken” * Optional function arguments ```lua function divide(numerator: number, denominator: number): Option if denominator == 0 then None() else return Some(numerator / denominator) end end ``` ]=] local Option = {} export type Option = typeof(Option) & { _optValue: T?, typeId: "Option", } --[=[ @within Option No value. ]=] function None(): Option return Option.new(nil) :: Option end --[=[ @within Option Some value of type `T`. ]=] function Some(val: T): Option return Option.new(val) :: Option end --[=[ @within Option Converts a potentially `nil` value into an [Option]. @param val T? -- The value to convert into an [Option] @return Option ]=] function Option.new(val: T?) return setmetatable( { _optValue = val, typeId = "Option", } :: Option, { __index = Option, __tostring = function(self: Option) if self._optValue == nil then return `{self.typeId}::None` else return `{self.typeId}::Some({self._optValue})` end end, __eq = function(self: Option, other: Option): boolean if typeof(self._optValue) == "table" and typeof(other._optValue) == "table" then return tableEq(self._optValue, other._optValue) else return self._optValue == other._optValue end end, __lt = function(self: Option, other: Option): boolean if self:isSome() and other:isSome() then return (self._optValue :: any) < (other._optValue :: any) end return false end, __le = function(self: Option, other: Option): boolean if self:isSome() and other:isSome() then return (self._optValue :: any) <= (other._optValue :: any) end return false end, } -- TODO: Implement __iter, once iterators traits exist ) end --[=[ @within Option Returns `true` is the [Option] is an [Option:Some] value. @param self Option @return boolean ]=] function Option.isSome(self: Option): boolean return self._optValue ~= nil end --[=[ @within Option Returns `true` is the [Option] is an [Option:Some] value and the value inside of it matches a predicate. @param self Option @param op (val: T) -> boolean -- The predicate function @return boolean ]=] 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 --[=[ @within Option Returns `true` is the [Option] is an [Option:None] value. @param self Option @return boolean ]=] function Option.isNone(self: Option): boolean return not self:isSome() end --[=[ @within Option Returns the [Option:Some]. @error panic -- Panics if there is an [Option:None] with a custom panic message provided by `msg`. ```lua local x: Option = Some("value") assert(x:expect("fruits are healthy") == "value") ``` ```lua local x: Option = None() x:expect("fruits are healthy") -- panics with `fruits are healthy` ``` @param self Option @param msg string -- The panic message @return boolean ]=] 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 --[=[ @within Option Returns the [Option:Some]. Because this function may panic, its use is generally discouraged. Instead, prefer to use [Option:unwrapOr], [Option:unwrapOrElse], or [Option:unwrapOrDefault]. @error panic -- Panics if the self value is [Option:None]. @param self Option @return T | never ]=] 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 --[=[ @within Option Returns the contained [Option:Some] value or a provided default. Arguments passed to unwrap_or are eagerly evaluated; if you are passing the result of a function call, it is recommended to use [Option:unwrapOrElse], which is lazily evaluated. ```lua assert(Some("car"):unwrapOr("bike") == "car") assert(None():unwrapOr("bike") == "bike") ``` @param self Option @param default T -- The default value @return T ]=] function Option.unwrapOr(self: Option, default: T): T if self:isSome() then return self._optValue :: T end return default end --[=[ @within Option Returns the contained [Option:Some] value or computes it from a function. ```lua local k = 10 assert(Some(4).unwrapOrElse(function() return 2 * k end) == 4) assert(None().unwrapOrElse(function() return 2 * k end) == 20) ``` @param self Option @param default () -> T -- The function which computes the default value @return T ]=] function Option.unwrapOrElse(self: Option, default: () -> T): T if self:isSome() then return self._optValue :: T end return default() end --[=[ @within Option Maps an [Option] to [Option] by applying a function to a contained value (if [Option:Some]) or returns [Option:None](if [Option:None]). ```lua local maybeSomeString: Option = Some("Hello, World!") local maybeSomeLen: Option = maybeSomeString:map(function(s) return #s end) assert(maybeSomeLen == 13) local x: Option = None() assert(x:map(function(s) return #s end) == None()) ``` @param self Option @param op (x: T) -> U? -- The function to apply @return Option ]=] 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 --[=[ @within Option Calls the provided closure with the contained value (if [Option:Some]). ```lua local v = { 1, 2, 3, 4, 5 } -- prints "got: 4" local x: Option = Option.new(v[4]):inspect(function(x) print("got: " .. x) end) -- prints nothing local x: Option = Option.new(v[5]):inspect(function(x) print("got: " .. x) end) ``` @param self Option @param op (x: T) -> () -- The function to call @return Option ]=] function Option.inspect(self: Option, op: (x: T) -> ()): Option if self:isSome() then op(self._optValue :: T) end return self end --[=[ @within Option Returns the provided default result (if none), or applies a function to the contained value (if any). Arguments passed to [Option:mapOr] are eagerly evaluated; if you are passing the result of a function call, it is recommended to use [Option:mapOrElse], which is lazily evaluated. ```lua local x: Option = Some("foo") assert(x.mapOr(42, function(v) return #v end) == 3) local x: Option = None() assert(x.mapOr(42, function(v) return #v end) == 42) ``` @param self Option @param default U -- The default value @param op (val: T) -> U -- The function to apply @return Option ]=] function Option.mapOr(self: Option, default: U, op: (val: T) -> U) if self:isSome() then return op(self._optValue :: T) end return default end --[=[ @within Option Computes a default function result (if none), or applies a different function to the contained value (if any). ```lua local k = 21; local x: Option = Some("foo") assert( x:mapOrElse( function() return 2 * k end, function(v) return #v end ) == 3 ) local x: Option = None() assert( x:mapOrElse( function() return 2 * k end, function(v) return #v end ) == 42 ) ``` @param self Option @param default () -> U -- The function to compute the default value @param op (val: T) -> U -- The function to apply @return Option ]=] 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(): never return error("Unimplemented: `Option:iter()`") end --[=[ @within Option Returns [Option:None] if the option is [Option:None], otherwise returns `optb`. Arguments passed to and are eagerly evaluated; if you are passing the result of a function call, it is recommended to use [Option:andThen], which is lazily evaluated. ```lua local x: Option = Some(2) local y: Option = None() assert(x:and_(y) == None()) local x: Option = None() local y: Option = Some("foo") assert(x:and_(y) == None()) local x: Option = Some(2) local y: Option = Some("foo") assert(x:and_(y) == Some("foo")) local x: Option = None() local y: Option = None() assert(x:and_(y), None()) ``` @param self Option @param optb Option -- The other option @return Option ]=] function Option.and_(self: Option, optb: Option): Option if self:isSome() then return optb end return None() end --[=[ @within Option Returns [Option:None] if the option is [Option:None], otherwise calls `op` with the wrapped value and returns the result. Some languages call this operation flatmap. ```lua function sqThenToString(x: number): Option return Option.new(x ^ 2):map(function(sq) return tostring(sq) end) end assert(Some(2):andThen(sqThenToString) == Some(tostring(4))) assert(None():andThen(sqThenToString) == None()) ``` Often used to chain fallible operations that may return [Option:None]. ```lua local arr2d = { { "A0", "A1" }, { "B0", "B1" } } local item01: Option = Option.new(arr2d[1]):andThen(function(row) return row[2] end) assert(item01 == Some("A1")) local item20: Option = Option.new(arr2d[3]):andThen(function(row) return row[0] end) assert(item20 == None()) ``` @param self Option @param op (val: T) -> Option -- The function to call @return Option ]=] function Option.andThen(self: Option, op: (val: T) -> Option): Option if self:isSome() then return op(self._optValue :: T) end return None() end --[=[ @within Option Returns [Option:None] if the option is [Option:None], otherwise calls `predicate` with the wrapped value and returns: * [Option:Some](t) if predicate returns `true` (where `t` is the wrapped value), and * [Option:None] if predicate returns `false`. ```lua function isEven(n: number): boolean return n % 2 == 0 end assert(None():filter(isEven) == None()) assert(Some(3):filter(isEven) == None()) assert(Some(4):filter(isEven) == Some(4)) ``` @param self Option @param predicate (val: T) -> boolean -- The predicate function which must match an element @return Option ]=] 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 --[=[ @within Option Returns the option if it contains a value, otherwise returns `optb`. Arguments passed to [Option:or_] are eagerly evaluated; if you are passing the result of a function call, it is recommended to use [Option:orElse], which is lazily evaluated. ```lua local x: Option = Some(2) local y: Option = None() assert(x:or_(y) == Some(2)) local x: Option = None() local y: Option = Some(100) assert(x:or_(y) == Some(100)) local x: Option = Some(2) local y: Option = Some(100) assert(x:or_(y) == Some(2)) local x: Option = None() local y: Option = None() assert(x:or_(y), None()) ``` @param self Option @param optb Option -- The other option @return Option ]=] function Option.or_(self: Option, optb: Option): Option if self:isSome() then return self end return optb end --[=[ @within Option Returns the option if it contains a value, otherwise calls `op` and returns the result. ```lua function nobody(): Option return None() end function vikings(): Option return Some("vikings") end assert(Some("barbarians"):orElse(vikings) == Some("barbarians")) assert(None():orElse(vikings) == Some("vikings")) assert(None():orElse(nobody) == None()) ``` @param self Option @param op () -> Option -- The function to call @return Option ]=] function Option.orElse(self: Option, op: () -> Option): Option if self:isSome() then return self end return op() end --[=[ @within Option Returns [Option:Some] if exactly one of `self`, `optb` is [Option:Some], otherwise returns [Option:None]. ```lua local x: Option = Some(2) local y: Option = None() assert(x:xor(y) == Some(2)) local x: Option = None() local y: Option = Some(2) assert(x:xor(y) == Some(2)) local x: Option = Some(2) local y: Option = Some(2) assert(x:xor(y) == None()) local x: Option = None() local y: Option = None() assert(x:xor(y) == None()) ``` @param self Option @param optb Option -- The other option @return Option ]=] 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 --[=[ @within Option Inserts value into the option, then returns it. If the option already contains a value, the old value is dropped. See also [Option:getOrInsert], which doesn’t update the value if the option already contains [Option:Some]. ```lua local opt: Option = None() local val: number = opt:insert(1) assert(val == 1) assert(opt:unwrap() == 1) local val: number = opt:insert(2) assert(val == 2) ``` @param self Option @param val T -- The value to insert @return T ]=] function Option.insert(self: Option, val: T): T self._optValue = val return self._optValue :: T end --[=[ @within Option Inserts value into the option, then returns it. If the option already contains a value, the old value is dropped. See also [Option:getOrInsert], which doesn’t update the value if the option already contains [Option:Some]. ```lua local opt: Option = None() local val: number = opt:insert(1) assert(val == 1) assert(opt:unwrap() == 1) local val: number = opt:insert(2) assert(val == 2) ``` @param self Option @param val T -- The value to insert @return T ]=] function Option.getOrInsert(self: Option, val: T): T if self:isNone() then self._optValue = val end return self._optValue :: T end --[=[ @within Option Takes the value out of the option, leaving an [Option:None] in its place. ```lua local x: Option = Some(2) local y: Option = x.take() assert(x == None()) assert(y == Some(2)) local x: Option = None() local y: Option = x.take() assert(x == None()) assert(y == None()) ``` @param self Option @return Option ]=] 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 --[=[ @within Option Replaces the actual value in the option by the value given in parameter, returning the old value if present, leaving an [Option:Some] in its place without deinitializing either one. ```lua local x: Option = Some(2) local old: Option = x:replace(5) assert(x == Some(5)) assert(old == Some(2)) local x: Option = None() local old: Option = x:replace(3) assert(x == Some(3)) assert(old == None()) ``` @param self Option @param val T @return Option ]=] 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 --[=[ @within Option Returns true if `val` is contained in the [Option:Some]. ```lua local x: Option = Some(2) local y: Option = None() assert(x:contains(2)) assert(x:contains(4)) assert(not y:contains(2)) ``` @param self Option @param val T @return boolean ]=] function Option.contains(self: Option, val: T): boolean if self:isSome() then return self._optValue == val end return false end --[=[ @within Option Zips `self` with another [Option]. If `self` is [Option:Some](s) and other is [Option:Some](o), this method returns [Option:Some]({s, o}). Otherwise, [Option:None] is returned. ```lua local x: Option = Some(1) local y: Option = Some("hi") local z: Option = None() assert(x:zip(y) == Some({ 1, "hi" })) assert(x:zip(z) == None()) ``` @param self Option @param other Option>U> @return Option<{T | U}> ]=] 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 --[=[ @within Option Zips `self` and another [Option] with function `op`. If `self` is [Option:Some](s) and other is [Option:Some](o), this method returns [Option:Some](op(s, o)). Otherwise, [Option:None] is returned. ```lua type Point = { x: number, y: number, } local Point: Point & { new: (x: number, y: number) -> Point, } = {} function Point.new(x: number, y: number): Point return { x = x, y = y, } end local xCoord: Option = Some(17.5) local yCoord: Option = Some(42.7) assert(xCoord:zipWith(yCoord, Point.new), Some({ x = 17.5, y = 42.7 })) assert(x:zipWith(None(), Point.new), None()) ``` @param self Option @param other Option>U> @param op (x: T, y: U) -> R? @return Option ]=] 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 --[=[ @within Option Unzips an option containing a table of two options. If `self` is `Some({a, b})` this method returns `(Some(a), Some(b))`. Otherwise, `(None(), None())` is returned. ```lua local x: Option<{ number | string }> = Some({ 1, "hi" }) local y: Option<{ number }> = None() assert((x:unzip() == Some(1), Some("hi"))) assert((y:unzip() == None(), None())) ``` @param self Option @return (Option, Option) ]=] 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 --[=[ @within Option Returns the inner value wrapped by the [Option]. ```lua local x: Option = Some("lol") local y: Option = None() assert(x:getInner() == "lol") assert(y:getInner() == nil) ``` @param self Option @return T? ]=] function Option.getInner(self: Option): T? return self._optValue end --[=[ @within Option Returns a formatted representation of the option, often used for printing to stdout. ```lua local x: Option = Some(123) local y: Option = None() print(x:display()) -- prints `Option::Some(123)` print(y:display()) -- prints `Option::None` ``` @param self Option @return string ]=] function Option.display(self: Option): string return tostring(self) end return { Option = Option, Some = Some, None = None, }