From a9c49708de6cbad2bd05c92738e3e142484cbb32 Mon Sep 17 00:00:00 2001 From: Hugo Devillers Date: Mon, 4 Sep 2023 15:07:11 +0200 Subject: [PATCH] Added documentation about implicits and introduce them in README.md --- README.md | 14 +++++ doc/implicits.md | 159 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 doc/implicits.md diff --git a/README.md b/README.md index ebaea53..0da1150 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,20 @@ fn select[T](cond: bool, a: T, b: T) -> T { fn main() -> i32 { S[i32] { elem = select[i32](true, 0, 1) }.elem } +``` + - __Experimental__: [Implicits](doc/implicits.md) provide a form of ad-hoc polymorphism, similar to Rust traits: +```rust +struct Vec3f { + x: f32, y: f32, z: f32 +} +struct Len[T] { + len: fn(T) -> f32 +} + +implicit = Len[Vec3f] { + len = | v | sqrt(v.x * v.x + v.y * v.y + v.z * v.z) +}; +fn longest(a: Vec3f, b: Vec3f, implicit l: Len[Vec3f]) = if (l.len(a) > l.len(b)) { a } else { b }; ``` - The type inference algorithm is now bidirectional type checking, which means that type information is propagated _locally_, not globally. This gives improved diff --git a/doc/implicits.md b/doc/implicits.md new file mode 100644 index 0000000..c5fb316 --- /dev/null +++ b/doc/implicits.md @@ -0,0 +1,159 @@ +# Implicits + +Artic experimentally supports ad-hoc polymorphism through implicit value declarations. +The keyword `implicit` is used in front of function parameters to indicate the parameter value may be omitted when calling the function. +When no value for such a parameter is provided, Artic will search for a suitable `implicit` declaration in the enclosing scopes. + +Here is a simple example: + +```rust +implicit i32 = 42; + +#[export] +fn the_answer(implicit i: i32) = i; +``` + +Calling `the_answer()` will yield `42`, because of the `implicit i32 = 42;` declaration on the first line. +Note that implicit declarations are not named, but instead use the type as a unique identifier. + +## Syntax Guide + +Implicit parameters should always be placed at the end of a function parameter list. + +For shorter implicit declarations, it is possible to omit the type on the left-hand side of the assignment. +Be mindful however, in such cases type inference will be invoked to type the implicit, and there is a risk the type it comes up with is not the expected one. + +```rust +struct T { + x: i32, + y: i32 +} + +implicit = T { x = 4, y = 2 }; +// instead of +implicit T = T { x = 4, y = 2 }; +``` + +Implicits are resolved in a scoping-sensitive manner, which includes modules and function bodies, +allowing for contextual availability and some degree of specialization: + +```rust +struct Emergency { + number: i32 +} + +fn @call_help(implicit e: Emergency) = e.number; + +mod usa { + implicit super::Emergency = super::Emergency { number = 911 }; + + #[export] + fn fire() = super::call_help(); +} + +mod eu { + implicit super::Emergency = super::Emergency { number = 112 }; + + #[export] + fn fire() = super::call_help(); + + #[export] + fn fire_but_in_belgium() -> i32 { + implicit super::Emergency = super::Emergency { number = 100 }; + return (super::call_help()) + } +} +``` + +It is possible to directly invoke the implicit solver through the `summon` keyword. +This is mostly useful for debugging the compiler and helps understanding how implicits are lowered, +however it is not recommended to use it in programs and it will likely be made unaccessible to user code in future updates. + +```rust +implicit i32 = 42; + +fn foo() -> i32 { summon[i32] } +``` + +## In practice + +Implicits can be used like Rust or Haskell traits, to implement functionality only available on certain types: + +```rust +struct Zero[T] { value: T } +struct Cmp[T] { cmp: fn(T, T) -> bool } + +implicit = Zero[i32] { value = 0 }; +implicit = Cmp[i32] { cmp = |x, y| x == y }; + +fn is_zero[T](value: T, implicit zero: Zero[T], implicit cmp: Cmp[T]) -> bool { + cmp.cmp(value, zero.value) +} + +#[export] +fn foo(i: i32) = if (is_zero[i32](i)) { 1 } else { 0 }; +``` + +Implicit parameters serve as an implicit declaration of their own: this means that within the scope of a function that +requires an implicit parameter, we can call other functions that require some or all of those parameters without needing +to bring in any implicit declaration: + +```rust +fn is_default_or_zero[T](value: T, default: T, implicit zero: Zero[T], implicit cmp: Cmp[T]) -> bool { + cmp.cmp(value, default) || is_zero(value) +} +``` + +Dummy wrapper types can be used to disambiguate between different implicit values that otherwise are the same: + +```rust +struct TheAnswer { + number: i32 +} + +implicit TheAnswer = TheAnswer { number = 42 }; + +struct NiceNumber { + number: i32 +} + +implicit NiceNumber = NiceNumber { number = 69 }; + +fn foo(implicit a: TheAnswer, implicit f: NiceNumber) {} +``` + +## TODO / Future features + +Currently implicit declarations only support non-generic values. +The implicits feature is considered a work in progress, and current syntax, and the design at large is susceptible to change. +In the future, we'll introduce support for both generic declarations, and derived implicit declarations. +Please express feedback to us loudly and clearly in GitHub issues if you'd like to shape the future of this feature. + +Generic declarations are just that, available for any type: + +``` +#[import(cc = "builtin")] fn undef[T]() -> T; + +implicit Zero[T] = Zero[T] { value = undef[T]() /* technically correct, the best kind of correct */ } +``` + +While derived implicit declarations are like functions that get invoked in order to generate the appropriate implicit. +Those functions can have implicit parameters of their own, which allows building up interesting abstractions: + +```rust +struct IsZero[T] { + is_zero: fn (T) -> bool +} + +implicit IsZero[i32](implicit cmp: Cmp[i32]) { + is_zero = | v | cmp.cmp(0, v) +}; +``` + +The real fun begins however, when we merge the two, allowing to define rich abstract interfaces: + +```rust +implicit IsZero[T](implicit zero: Zero[T], implicit cmp: Cmp[T]) { + is_zero = | v | cmp.cmp(zero.zero, v) +}; +``` \ No newline at end of file