Skip to content

Commit dc588b1

Browse files
committed
New abstract type section
1 parent dee2468 commit dc588b1

1 file changed

Lines changed: 270 additions & 3 deletions

File tree

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

Lines changed: 270 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,280 @@ 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+
Using associated types within zero-arity traits introduces a concept I 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 key property of an abstract type is that it is not tied to any particular value type. Its concrete form is chosen by the ambient context. We can modify the earlier `GreetHello` implementation to use the abstract `Name` 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 replaced by the abstract type `HasNameType::Name`. This allows the same provider to be reused across different name types, whichever the context chooses to wire in.
1139+
1140+
For example, we can define a `FullName` type and use it as the concrete `Name`:
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+
The `Context` type wires `FullName` as the concrete `Name` through `NameTypeProviderComponent`, and wires `GreetHello` as the greeting implementation. Because `GreetHello` uses `HasNameType::Name` rather than a hardcoded `String`, it adapts automatically to the `FullName` type without any changes to the provider itself.
1181+
1182+
#### Abstract types in Incoherent Rust
1183+
1184+
To see how abstract types might look in Incoherent Rust, we would start by defining `HasNameType` as a zero-arity trait:
1185+
1186+
```rust title="Incoherent Rust"
1187+
pub trait HasNameType for ! {
1188+
type Name: Display;
1189+
}
1190+
```
1191+
1192+
Since abstract types are built on top of zero-arity traits, using them requires new syntax to bind and reference the abstract type:
1193+
1194+
```rust title="Incoherent Rust"
1195+
impl GreetHello = Greet for !
1196+
where
1197+
impl NameType: HasNameType,
1198+
with
1199+
name: &NameType::Name,
1200+
{
1201+
fn greet() {
1202+
println!("Hello, {}!", name);
1203+
}
1204+
}
1205+
```
1206+
1207+
Here, we require the context to provide an implementation of `HasNameType` and bind it to the local identifier `NameType`. The abstract type is then referenced as `NameType::Name` when specifying the type of the `name` capability.
1208+
1209+
With the implementation in place, we can set up the wiring and call `GreetHello::greet()` as follows:
1210+
1211+
```rust title="Incoherent Rust"
1212+
impl UseFullNameType = HasNameType {
1213+
type Name = FullName;
1214+
}
1215+
1216+
fn main() {
1217+
with
1218+
UseFullNameType,
1219+
name = FullName {
1220+
first_name: "John".to_owned(),
1221+
last_name: "Smith".to_owned(),
1222+
}
1223+
{
1224+
// Prints "Hello, John Smith!"
1225+
GreetHello::greet();
1226+
}
1227+
}
1228+
```
1229+
1230+
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.
1231+
1232+
#### Using abstract types across incoherent traits
1233+
1234+
With the earlier abstract `Name` type as background, it might not be obvious why defining an associated type through a zero-arity trait is preferable to implementing everything with normal unary traits. Thus we will show a more concrete example of how zero-arity traits provide uniform access across multiple unary trait implementations that have different `Self` types.
1235+
1236+
Suppose we want to implement a generic `touch` function that retrieves the current system time and updates the `last_modified` field of any type that supports it. We might start with a straightforward trait definition:
1237+
1238+
```rust title="Rust"
1239+
use std::time::SystemTime;
1240+
1241+
pub trait UpdateLastModified {
1242+
fn update_last_modified(&mut self, time: SystemTime);
1243+
}
1244+
```
1245+
1246+
The `UpdateLastModified` trait 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 value. With it, the `touch` function can be written as follows:
1247+
1248+
```rust title="Rust"
1249+
pub fn touch<T: UpdateLastModified>(value: &mut T) {
1250+
let time = SystemTime::now();
1251+
value.update_last_modified(time);
1252+
}
1253+
```
1254+
1255+
While this works, it ties the implementation to a specific concrete time type. If we want to support other time representations, 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), we need a way to abstract over the `Time` type.
1256+
1257+
One approach is to add `Time` as an explicit generic parameter:
1258+
1259+
```rust title="Rust"
1260+
pub trait UpdateLastModified<Time> {
1261+
fn update_last_modified(
1262+
&mut self,
1263+
time: Time,
1264+
);
1265+
}
1266+
1267+
pub fn touch<Time, T: UpdateLastModified<Time>>(
1268+
value: &mut T,
1269+
)
1270+
where
1271+
Time: From<SystemTime>,
1272+
{
1273+
let time = SystemTime::now();
1274+
value.update_last_modified(time);
1275+
}
1276+
```
1277+
1278+
Now `UpdateLastModified` is parameterized by a generic `Time` type, and `touch` must also accept `Time` as an extra parameter. Even so, the implementation still relies on `SystemTime::now()` internally and requires a `From` conversion from `SystemTime`, so the abstraction is only partial.
1279+
1280+
Another option is to make `Time` an associated type:
1281+
1282+
```rust title="Rust"
1283+
pub CurrentTime {
1284+
fn current_time() -> Self;
1285+
}
1286+
1287+
pub trait UpdateLastModified {
1288+
type Time: CurrentTime;
1289+
1290+
fn update_last_modified(
1291+
&self,
1292+
time: Self::Time,
1293+
);
1294+
}
1295+
1296+
pub fn touch<T: UpdateLastModified>(value: &mut T) {
1297+
let time = T::Time::current_time();
1298+
1299+
value.update_last_modified(time);
1300+
}
1301+
```
1302+
1303+
This version is cleaner, but it has a structural problem. Because `Time` is an associated type of `UpdateLastModified`, every type `T` that implements the trait carries its own independent `Time` type. There is no guarantee that two different value types share the same time representation, and `CurrentTime` is tightly coupled to the concrete `Time` type in each implementation. This makes it difficult to swap in a mock clock for testing, and makes it harder to reason about whether different types are using the same time and clock.
1304+
1305+
With zero-arity traits and Incoherent Rust, we can write the following instead:
1306+
1307+
```rust title="Incoherent Rust"
1308+
// An abstract type trait is a zero-arity trait with an associated type
1309+
pub trait HasTimeType for ! {
1310+
type Time;
1311+
}
1312+
1313+
// The current time is provided by with ambient context using a zero-arity trait
1314+
pub trait CurrentTime for !
1315+
where
1316+
impl TimeImpl: HasTimeType,
1317+
{
1318+
fn current_time() -> TimeImpl::Time
1319+
}
1320+
1321+
// The `Time` type does not depend on `Self`
1322+
pub trait UpdateLastModified
1323+
where
1324+
impl TimeImpl: HasTimeType,
1325+
{
1326+
fn update_last_modified(
1327+
&mut self,
1328+
time: TimeImpl::Time,
1329+
);
1330+
}
1331+
1332+
// The time type and current time implementations are not bound to `T`
1333+
pub incoherent fn touch<T: UpdateLastModified>(
1334+
value: &mut T,
1335+
)
1336+
where
1337+
impl TimeImpl: HasTimeType + CurrentTime,
1338+
{
1339+
let time = TimeImpl::current_time();
1340+
value.update_last_modified(time);
1341+
}
1342+
```
1343+
1344+
Zero-arity traits allow us to define implementations that are not bound to any particular `Self` type or generic parameter. When two different value types, such as `Folder` and `File`, both need `touch` to be called on them, they can share the same time implementation through the ambient context rather than each carrying their own independent copy.
1345+
1346+
The same example can be expressed in CGP today as:
1347+
1348+
```rust title="CGP"
1349+
#[cgp_type]
1350+
pub trait HasTimeType {
1351+
type Time;
1352+
}
1353+
1354+
#[cgp_component(CurrentTimeImpl)]
1355+
pub trait CurrentTime: HasTimeType {
1356+
fn current_time(&self) -> Self::Time;
1357+
}
1358+
1359+
#[cgp_component(UpdateLastModifiedImpl)]
1360+
#[use_type(HasTimeType::Time)]
1361+
pub trait UpdateLastModified<T> {
1362+
fn update_last_modified(value: &mut T, time: Time);
1363+
}
1364+
1365+
#[cgp_fn]
1366+
#[uses(CurrentTime)]
1367+
pub fn touch<T>(&self, value: &mut T) {
1368+
let time = self.current_time();
1369+
value.update_last_modified(time);
1370+
}
1371+
```
1372+
1373+
CGP converts zero-arity traits into single-arity traits that are bound to the generic context, while the original `Self` type from the Incoherent Rust version becomes an explicit generic parameter `T`. With this structure, traits like `HasTimeType` and `CurrentTime` are clearly bound to the context rather than to any particular value type. This example makes concrete the benefit of abstract types anchored to the ambient context: they allow multiple value types to share a consistent, interchangeable abstraction without requiring each one to carry its own copy.
1374+
1375+
11091376
### Contexts as explicit types
11101377

11111378
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)