Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge experimental support for Implicits #19

Merged
merged 20 commits into from
Sep 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
159 changes: 159 additions & 0 deletions doc/implicits.md
Original file line number Diff line number Diff line change
@@ -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)
};
```
Loading
Loading