diff options
-rw-r--r-- | BASIC.md | 1 | ||||
-rw-r--r-- | ERRORS.md | 24 | ||||
-rw-r--r-- | std/default/options.pk | 87 | ||||
-rw-r--r-- | std/default/results.pk | 123 |
4 files changed, 235 insertions, 0 deletions
@@ -21,6 +21,7 @@ The type system is comprehensive, and complex enough to [warrant its own documen Functions are declared with the `func` keyword, followed by the function name, followed by an (optional) list of parameters surrounded in parenthesis, followed by a type annotation. Functions may be prefixed with one or more of the following modifiers: - `pub`: exports the function for use by external files - `pure`: denotes a function as a "pure function", lacking side effects, i.e. IO or nondeterminism or parameter mutability +- `yeet`: denotes a function as a "throwing function", meaning it may raise exceptions. - `async`: marks a function as asynchronous which may only be called by other asynchronous functions or with the `await` keyword <!-- - `total`: idk --> <!-- - more?? converter?? idk todo --> diff --git a/ERRORS.md b/ERRORS.md new file mode 100644 index 0000000..4a4b206 --- /dev/null +++ b/ERRORS.md @@ -0,0 +1,24 @@ +# Error Handling + +Error handling should perhaps be abstracted into a more general effects system. +But if not, then this document lays out some potential ideas. + +--- + +```puck +``` + +Puck provides [`Option[T]`](std/default/options.pk) and a [`Result[T, E]`](std/default/results.pk) types, imported by default. These are `union` types and so must be pattern matched upon to be useful: but the standard library provides a bevy of helper functions. + +Two in particular are of note. The `?` operator unwraps a Result or propagates its error up a function call. The `!` operator unwraps an Option or Result directly or throws an exception in the case of None or Error. + +```puck +``` + +Errors raised by the `!` operator must be explicitly caught and handled via a `try/catch/finally` statement. + +If an exception is not handled within a function body, the function must be explicitly marked as a throwing function via the `yeet` prefix (final name to be determined). The compiler will statically determine which exceptions in particular are thrown from any given function. + +This creates a distinction between two types of error handling, working in sync: functional error handling with [Option](https://en.wikipedia.org/wiki/Option_type) and [Result](https://en.wikipedia.org/wiki/Result_type) types, and object-oriented error handling with [nullable types](https://en.wikipedia.org/wiki/Nullable_type) and [exceptions](https://en.wikipedia.org/wiki/Exception_handling). These styles may be swapped between with minimal syntax overhead. Libraries, however, should universally use Options and Results, as this provides best for both usages. + +References: [std/options](std/default/options.pk), [std/results](std/default/results.pk), [Error Handling in Swift](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/errorhandling) (shamelessly stolen) diff --git a/std/default/options.pk b/std/default/options.pk new file mode 100644 index 0000000..f1bcac4 --- /dev/null +++ b/std/default/options.pk @@ -0,0 +1,87 @@ +## std/options +## This module is imported by default. + +import std/format + +pub type Option[T] = union + Some: T + None + +pub func is_some[T](self: Option[T]): bool = + self of Some(_) +pub func is_none[T](self: Option[T]): bool = + not self.is_some + +## Converts an Option[T] to a Result[T, E] given a user-provided error. +pub func err[T, E](self: Option[T], error: E): Result[T, E] = + if self of Some(x): + Okay(x) + else: + Error(error) + +## Applies a function to T, if it exists. +pub func map[T, U](self: Option[T], proc: T -> U): Option[U] = + if self of Some(x): + Some(x.proc) + else: + None +## Converts T to a None, if proc returns false and it exists. +pub func filter[T](self: Option[T], proc: T -> bool): Option[T] = + if self of Some(x) and proc(x): + Some(x) + else: + None + +## Applies a function to T, if it exists. Equivalent to .map(func).flatten. +pub func flatmap[T, U](self: Option[T], proc: T -> Option[U]): Option[U] = + if self of Some(x): + x.proc + else: + None +## Converts from Option[Option[T]] to Option[T]. +pub func flatten[T](self: Option[Option[T]]): Option[T] = # todo: better name? + match self + of Some(Some(x)): + Some(x) + of _: + None + +## Returns the inner value or a default. +pub func or[T](self: Option[T], default: T): T = + if self of Some(x): x + else: default +## Directly accesses the inner value. Throws an exception if None. +pub yeet func get[T](self: Option[T]): T = + if self of Some(x): x + else: raise Exception # todo: syntax?? + +# todo: direct access, alias to get +macro `!`[T](self: Option[T]): T +# todo: indirect access, ??? do we propagate? is this useful? probably not +macro `?`[T](self: Option[T]): T +# todo: field access? useful? assignment? +macro `.?`[T](self: Option[T]) + +## Overloads the == operation for use on Options. +pub func `==`[T](a, b: Option[T]): bool = + match (a, b) + of (Some(x), Some(y)): + x == y + of _: + false + +## Overloads the str() function for use on Options. +pub func str[T](self: Option[T]): str = + if self of Some(x): + fmt("some({})", x.str) + else: + "none" + +examples: + let x = Some(42) + if x of Some(y): + assert x! == y + +# references: +# https://nim-lang.github.io/Nim/options.html +# https://doc.rust-lang.org/std/option/enum.Option.html diff --git a/std/default/results.pk b/std/default/results.pk new file mode 100644 index 0000000..2ac47d0 --- /dev/null +++ b/std/default/results.pk @@ -0,0 +1,123 @@ +## std/results +## This module is imported by default. + +import std/[options, format] + +pub type Result[T, E] = union + Okay: T + Error: E + +pub type Error = interface + func str(self: Self) + func dbg(self: Self) + +pub type Result[T] = Result[T, ref Error] + +pub func is_ok[T, E](self: Result[T, E]): bool = + self of Okay(_) +pub func is_err[T, E](self: Result[T, E]): bool = + not self.is_ok + +## Converts from a Result[T, E] to an Option[T]. +pub func ok[T, E](self: Result[T, E]): Option[T] = + if self of Okay(x): + Some(x) + else: + None() +## Converts from a Result[T, E] to an Option[E]. +pub func err[T, E](self: Result[T, E]): Option[E] = + if self of Error(x): + Some(x) + else: + None() + +## Applies a function to T, if self is Okay. +pub func map[T, E, U](self: Result[T, E], proc: T -> U): Result[U, E] = + match self + of Okay(x): + Okay(x.proc) + of Error(e): + Error(e) +## Applies a function to E, if self is Error. +pub func map_err[T, E, F](self: Result[T, E], proc: E -> F): Result[T, F] = + match self + of Error(e): + Error(e.proc) + of Okay(x): + Okay(x) + +## Applies a function to T, if it exists. Equivalent to .map(func).flatten. +pub func flatmap[T, E, U](self: Result[T, E], proc: T -> Result[U, E]): Result[U, E] = + match self + of Okay(x): + x.proc + of Error(e): + Error(e) +## Converts from a Result[Result[T, E], E] to a Result[T, E]. +pub func flatten[T, E](self: Result[Result[T, E], E]): Result[T, E] = + match self + of Okay(Okay(x)): + Okay(x) + of Okay(Error(e)), Error(e): + Error(e) + +## Transposes a Result[Option[T], E] to an Option[Result[T, E]]. +pub func transpose[T, E](self: Result[Option[T], E]): Option[Result[T, E]] = + match self + of Okay(Some(x)): + Some(Okay(x)) + of Okay(None()), Error(_): + None() +## Transposes an Option[Result[T, E]] to a Result[Option[T], E]. Takes a default error. +pub func transpose[T, E](self: Option[Result[T, E]], error: E): Result[Option[T], E] = + match self + of Some(Okay(x)): + Okay(Some(x)) + of Some(Error(e)): + Error(e) + of None(): + Error(error) + +## Returns the inner value or a default. +pub func or[T, E](self: Result[T, E], default: T): T = + if self of Okay(x): x + else: default +## Directly accesses the inner value. Throws an exception if Error(e). +pub yeet func get[T, E](self: Result[T, E]): T = + match self + of Okay(x): x + of Error(e): raise Exception(e) # todo: syntax?? +pub yeet func get_err[T, E](self: Result[T, E]): E = + match self + of Error(e): e + of Okay(x): raise Exception(x) # todo: syntax?? + +# todo: direct access, alias to get +macro `!`[T, E](self: Result[T, E]): T +# todo: indirect access, propagates Err +macro `?`[T, E](self: Result[T]): T + +## Overloads the == operation for use on Results. +pub func `==`[T, E, F](a: Result[T, E], b: Result[T, F]): bool = + match (a, b) + of (Okay(x), Okay(y)): + x == y + of _: + false + +## Overloads the str() function for use on Results. +pub func str[T, E](self: Result[T, E]): str = + match self + of Some(x): + fmt("Okay({})", x.str) + of Error(e): + fmt("Error({})", e.str) + +examples: + let x: Error("fuck") = Okay(42) + func idk: Result[int, string] + +# references: +# https://doc.rust-lang.org/std/result/enum.Result.html +# https://github.com/arnetheduck/nim-results +# https://github.com/codex-storage/questionable |