Skip to content

Commit 2d7c9b6

Browse files
committed
New abstract type section
1 parent dee2468 commit 2d7c9b6

1 file changed

Lines changed: 271 additions & 3 deletions

File tree

blog/2026-03-30-incoherent-rust-today.md

Lines changed: 271 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,7 @@ There is also an important way in which CGP diverges from traditional algebraic
10391039
These observations point toward a better fit in the [**coeffects**](https://tomasp.net/coeffects/) framework, which studies how contextual requirements flow through a computation rather than how computational effects flow out of it. Where effects describe what a computation produces or performs, coeffects describe what a computation requires from its environment. The generic context in CGP serves precisely this role: it carries the implementation choices and capabilities that a computation depends on, and those dependencies are resolved all at once when a concrete context is defined. A full introduction to coeffects is beyond the scope of this blog post, but the connection is worth noting for readers interested in the theoretical foundations of what CGP is doing.
10401040

10411041

1042-
### 0-arity traits
1042+
### Zero-arity traits
10431043

10441044
CGP provides a desugaring of incoherent Rust traits into CGP traits that have an additional `Context` type, with the original `Self` type moved to an explicit generic parameter. For example, given the incoherent trait:
10451045

@@ -1084,7 +1084,7 @@ With incoherence and capabilities, a useful design pattern that emerges is the u
10841084
```rust title="Incoherent Rust"
10851085
impl GreetHello = Greet for !
10861086
with
1087-
name: String,
1087+
name: &String,
10881088
{
10891089
fn greet() {
10901090
println!("Hello, {}!", name);
@@ -1099,13 +1099,281 @@ With CGP, since the context type is explicit, zero-arity incoherent traits becom
10991099
impl GreetImpl {
11001100
fn greet(
11011101
&self,
1102-
#[implicit] name: String,
1102+
#[implicit] name: &String,
11031103
) {
11041104
println!("Hello, {}!", name);
11051105
}
11061106
}
11071107
```
11081108

1109+
### Abstract types
1110+
1111+
There is an interesting implication of using associated types within zero-arity traits, which is that it introduces a new concepts that I'd call **abstract types**. For example, we can introduce an abstract type called `Name` with the following trait:
1112+
1113+
1114+
```rust title="CGP"
1115+
#[cgp_type]
1116+
pub trait HasNameType {
1117+
type Name: Display;
1118+
}
1119+
```
1120+
1121+
1122+
The interesting property about abstract types is that the type does not depend on any specific type, but rather is chosen by the ambient context. For example, we can modify the `GreetHello` implementation earlier to use the `Name` abstract type:
1123+
1124+
1125+
```rust title="CGP"
1126+
#[cgp_impl(new GreetHello)]
1127+
#[use_type(HasNameType::Name)]
1128+
impl GreetImpl {
1129+
fn greet(
1130+
&self,
1131+
#[implicit] name: &Name,
1132+
) {
1133+
println!("Hello, {}!", name);
1134+
}
1135+
}
1136+
```
1137+
1138+
In this updated implementation, the hardcoded `String` type for the `name` parameter is now replaced by the abstract type `HasNameType::Name`. This allows the implementation to be reused by different name types that may be chosen by the context.
1139+
1140+
For example, we can now define a type like `FullName`, and use it as the custom `Name` type:
1141+
1142+
```rust
1143+
pub struct FullName {
1144+
pub first_name: String,
1145+
pub last_name: String,
1146+
}
1147+
1148+
impl Display for FullName {
1149+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1150+
write!(f, "{} {}", self.first_name, self.last_name)
1151+
}
1152+
}
1153+
1154+
pub struct Context {
1155+
pub name: FullName,
1156+
}
1157+
1158+
delegate_components! {
1159+
Context {
1160+
NameTypeProviderComponent:
1161+
UseType<FullName>,
1162+
GreeterComponent:
1163+
GreetHello,
1164+
}
1165+
}
1166+
1167+
fn main() {
1168+
let context = Context {
1169+
name: FullName {
1170+
first_name: "John".to_owned(),
1171+
last_name: "Smith".to_owned(),
1172+
},
1173+
};
1174+
1175+
// Prints "Hello, John Smith!"
1176+
context.greet();
1177+
}
1178+
```
1179+
1180+
#### Abstract types in Incoherent Rust
1181+
1182+
Let's take a look at how abstract types would look like if we introduce it in Incoherent Rust. First, we would define `HasNameType` as a zero-arity trait as follows:
1183+
1184+
```rust title="Incoherent Rust"
1185+
pub trait HasNameType for ! {
1186+
type Name: Display;
1187+
}
1188+
```
1189+
1190+
Since abstract types are built on top of zero-arity traits, we would need to introduce new syntax to use the abstract type:
1191+
1192+
```rust title="Incoherent Rust"
1193+
impl GreetHello = Greet for !
1194+
where
1195+
impl NameType: HasNameType,
1196+
with
1197+
name: &NameType::Name,
1198+
{
1199+
fn greet() {
1200+
println!("Hello, {}!", name);
1201+
}
1202+
}
1203+
```
1204+
1205+
Here, we require the context to provide an implementation of `HasNameType`, and bind it to the local identifier `NameType`. We then refer to the abstract type using `NameType::Name` when using it as the type for the `name` capability.
1206+
1207+
Now that the implementation is ready, we can setup the wiring and call `GreetHello::greet()` by writing something like follows:
1208+
1209+
```rust title="Incoherent Rust"
1210+
impl UseFullNameType = HasNameType {
1211+
type Name = FullName;
1212+
}
1213+
1214+
fn main() {
1215+
with
1216+
UseFullNameType,
1217+
name = FullName {
1218+
first_name: "John".to_owned(),
1219+
last_name: "Smith".to_owned(),
1220+
}
1221+
{
1222+
// Prints "Hello, John Smith!"
1223+
GreetHello::greet();
1224+
}
1225+
}
1226+
```
1227+
1228+
Here, we choose `UseFullNameType` as the incoherent implementation for `HasNameType`, and then bind the capability `name` with a `FullName` value. When `GreetHello` is called, both the type implementation and the name value are provided through the trait system, and the greeting is printed.
1229+
1230+
#### Using abstract types across incoherent traits
1231+
1232+
With the earlier abstract `Name` type, it might not be clear what is the difference of defining the associated type through a zero-arity trait, as compared to just implementing everything using the normal unary traits. We will expand on it in this section to see how zero-arity traits provide uniform access across multiple unary trait implementations that have *different* `Self` types.
1233+
1234+
Suppose that we want to implement a generic `touch` function that gets the current system time, and then modify any type that contains the `last_modified` field. We may first define a trait like follows:
1235+
1236+
```rust
1237+
use std::time::SystemTime;
1238+
1239+
pub trait UpdateLastModified {
1240+
fn update_last_modified(
1241+
&mut self,
1242+
time: SystemTime,
1243+
);
1244+
}
1245+
```
1246+
1247+
The `UpdateLastModified` trait is straightforward: It accepts a `&mut self` and a [`SystemTime`](https://doc.rust-lang.org/stable/std/time/struct.SystemTime.html), and updates the time field in `self` to the given time. With it, we can define the `touch` function simply as follows:
1248+
1249+
```rust
1250+
pub fn touch<T: UpdateLastModified>(value: &mut T) {
1251+
let time = SystemTime::now();
1252+
value.update_last_modified(time);
1253+
}
1254+
```
1255+
1256+
While the `touch` function works, it introduces some tight coupling with the a specific concrete implementation of time. We may want to make use of a *generic* `Time` type that can be instantiated to different concrete time types, such as [`std::time::Instant`](https://doc.rust-lang.org/std/time/struct.Instant.html), [`time::UtcDatetime`](https://docs.rs/time/latest/time/struct.UtcDateTime.html), or [`chrono::DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html).
1257+
1258+
A naive attempt would be to add a generic parameter `Time` as follows:
1259+
1260+
```rust
1261+
pub trait UpdateLastModified<Time> {
1262+
fn update_last_modified(
1263+
&mut self,
1264+
time: Time,
1265+
);
1266+
}
1267+
1268+
pub fn touch<Time, T: UpdateLastModified<Time>>(
1269+
value: &mut T,
1270+
)
1271+
where
1272+
Time: From<SystemTime>,
1273+
{
1274+
let time = SystemTime::now();
1275+
value.update_last_modified(time);
1276+
}
1277+
```
1278+
1279+
Now the `UpdateLastModified` trait is parameterized by a generic `Time` type, and the `touch` function also needs to accept `Time` as an extra generic parameter. Furthermore, even though our code is generic over the `Time` type, it still uses `SystemTime::now()` to obtain the current time, and require a `From` conversion from `SystemTime` to the generic `Time`.
1280+
1281+
An alternative is that we can make `Time` an associated type. Such as:
1282+
1283+
```rust
1284+
pub CurrentTime {
1285+
fn current_time() -> Self;
1286+
}
1287+
1288+
pub trait UpdateLastModified {
1289+
type Time: CurrentTime;
1290+
1291+
fn update_last_modified(
1292+
&self,
1293+
time: Self::Time,
1294+
);
1295+
}
1296+
1297+
pub fn touch<T: UpdateLastModified>(value: &mut T) {
1298+
let time = T::Time::current_time();
1299+
1300+
value.update_last_modified(time);
1301+
}
1302+
```
1303+
1304+
But the definition don't exactly sit right. First, there is now a different `Time` type for every type `T` that implements `UpdateLastModified`. Furthermore, the trait `CurrentTime` is tightly coupled with the concrete `Time` definition. This makes it challenging to swap a mock implementation to be used for testing.
1305+
1306+
With zero-arity traits and Incoherent Rust, we can instead write something like the following:
1307+
1308+
```rust
1309+
// An abstract type trait is a zero-arity trait with an associated type
1310+
pub trait HasTimeType for ! {
1311+
type Time;
1312+
}
1313+
1314+
// The current time is provided by with ambient context using a zero-arity trait
1315+
pub trait CurrentTime for !
1316+
where
1317+
impl TimeImpl: HasTimeType,
1318+
{
1319+
fn current_time() -> TimeImpl::Time
1320+
}
1321+
1322+
// The `Time` type does not depend on `Self`
1323+
pub trait UpdateLastModified
1324+
where
1325+
impl TimeImpl: HasTimeType,
1326+
{
1327+
fn update_last_modified(
1328+
&mut self,
1329+
time: TimeImpl::Time,
1330+
);
1331+
}
1332+
1333+
// The time type and current time implementations are not bound to `T`
1334+
pub incoherent fn touch<T: UpdateLastModified>(
1335+
value: &mut T,
1336+
)
1337+
where
1338+
impl TimeImpl: HasTimeType + CurrentTime,
1339+
{
1340+
let time = TimeImpl::current_time();
1341+
value.update_last_modified(time);
1342+
}
1343+
```
1344+
1345+
As we can see, zero-arity traits allows us to define `Self`-less implementations that are not bound to any generic parameters. This also allows us to be reuse the same zero-arity trait implementations across multiple generic types. For example, when there are two generic types `Folder` and `File`, we can be confident that both of them would reuse the same time implementations within the same call.
1346+
1347+
The same zero-trait example above can be defined in CGP today as:
1348+
1349+
```rust
1350+
#[cgp_type]
1351+
pub trait HasTimeType {
1352+
type Time;
1353+
}
1354+
1355+
#[cgp_component(CurrentTimeImpl)]
1356+
pub trait CurrentTime: HasTimeType {
1357+
fn current_time(&self) -> Self::Time;
1358+
}
1359+
1360+
#[cgp_component(UpdateLastModifiedImpl)]
1361+
#[use_type(HasTimeType::Time)]
1362+
pub trait UpdateLastModified<T> {
1363+
fn update_last_modified(value: &mut T, time: Time);
1364+
}
1365+
1366+
#[cgp_fn]
1367+
#[uses(CurrentTime)]
1368+
pub fn touch<T>(&self, value: &mut T) {
1369+
let time = self.current_time();
1370+
value.update_last_modified(time);
1371+
}
1372+
```
1373+
1374+
As we can see, CGP converts zero arity traits into single-arity traits that are bound to the generic context. On the other hand, the original `Self` type in Incoherent Rust is shifted to become explicit generic parameters `T`. With this, it becomes clear that traits like `HasTimeType` and `CurrentTime` are bound to the context instead of the generic `T` type. This example should make it clear the benefits of zero-arity traits and abstract types that are bound to the ambient context instead of the value types.
1375+
1376+
11091377
### Contexts as explicit types
11101378

11111379
Another key distinction is that CGP contexts are explicit Rust types. This means we can use the context as a regular Rust type inside a larger context. Incoherent Rust, on the other hand, assumes an implicit ambient context that propagates named impls from callers to callees. This makes it more restrictive to use Incoherent Rust when implementing traits for concrete types.

0 commit comments

Comments
 (0)