You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: blog/2026-03-30-incoherent-rust-today.md
+270-3Lines changed: 270 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1039,7 +1039,7 @@ There is also an important way in which CGP diverges from traditional algebraic
1039
1039
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.
1040
1040
1041
1041
1042
-
### 0-arity traits
1042
+
### Zero-arity traits
1043
1043
1044
1044
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:
1045
1045
@@ -1084,7 +1084,7 @@ With incoherence and capabilities, a useful design pattern that emerges is the u
1084
1084
```rust title="Incoherent Rust"
1085
1085
implGreetHello=Greetfor!
1086
1086
with
1087
-
name:String,
1087
+
name:&String,
1088
1088
{
1089
1089
fngreet() {
1090
1090
println!("Hello, {}!", name);
@@ -1099,13 +1099,280 @@ With CGP, since the context type is explicit, zero-arity incoherent traits becom
1099
1099
implGreetImpl {
1100
1100
fngreet(
1101
1101
&self,
1102
-
#[implicit] name:String,
1102
+
#[implicit] name:&String,
1103
1103
) {
1104
1104
println!("Hello, {}!", name);
1105
1105
}
1106
1106
}
1107
1107
```
1108
1108
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
+
pubtraitHasNameType {
1117
+
typeName: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
+
implGreetImpl {
1129
+
fngreet(
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`:
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
+
pubtraitHasNameTypefor! {
1188
+
typeName: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
+
implGreetHello=Greetfor!
1196
+
where
1197
+
implNameType:HasNameType,
1198
+
with
1199
+
name:&NameType::Name,
1200
+
{
1201
+
fngreet() {
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
+
implUseFullNameType=HasNameType {
1213
+
typeName=FullName;
1214
+
}
1215
+
1216
+
fnmain() {
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:
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
+
pubfntouch<T:UpdateLastModified>(value:&mutT) {
1250
+
lettime=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
+
pubtraitUpdateLastModified<Time> {
1261
+
fnupdate_last_modified(
1262
+
&mutself,
1263
+
time:Time,
1264
+
);
1265
+
}
1266
+
1267
+
pubfntouch<Time, T:UpdateLastModified<Time>>(
1268
+
value:&mutT,
1269
+
)
1270
+
where
1271
+
Time:From<SystemTime>,
1272
+
{
1273
+
lettime=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
+
pubCurrentTime {
1284
+
fncurrent_time() ->Self;
1285
+
}
1286
+
1287
+
pubtraitUpdateLastModified {
1288
+
typeTime:CurrentTime;
1289
+
1290
+
fnupdate_last_modified(
1291
+
&self,
1292
+
time:Self::Time,
1293
+
);
1294
+
}
1295
+
1296
+
pubfntouch<T:UpdateLastModified>(value:&mutT) {
1297
+
lettime=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
+
pubtraitHasTimeTypefor! {
1310
+
typeTime;
1311
+
}
1312
+
1313
+
// The current time is provided by with ambient context using a zero-arity trait
1314
+
pubtraitCurrentTimefor!
1315
+
where
1316
+
implTimeImpl:HasTimeType,
1317
+
{
1318
+
fncurrent_time() ->TimeImpl::Time
1319
+
}
1320
+
1321
+
// The `Time` type does not depend on `Self`
1322
+
pubtraitUpdateLastModified
1323
+
where
1324
+
implTimeImpl:HasTimeType,
1325
+
{
1326
+
fnupdate_last_modified(
1327
+
&mutself,
1328
+
time:TimeImpl::Time,
1329
+
);
1330
+
}
1331
+
1332
+
// The time type and current time implementations are not bound to `T`
1333
+
pubincoherentfntouch<T:UpdateLastModified>(
1334
+
value:&mutT,
1335
+
)
1336
+
where
1337
+
implTimeImpl:HasTimeType+CurrentTime,
1338
+
{
1339
+
lettime=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
+
pubtraitHasTimeType {
1351
+
typeTime;
1352
+
}
1353
+
1354
+
#[cgp_component(CurrentTimeImpl)]
1355
+
pubtraitCurrentTime:HasTimeType {
1356
+
fncurrent_time(&self) ->Self::Time;
1357
+
}
1358
+
1359
+
#[cgp_component(UpdateLastModifiedImpl)]
1360
+
#[use_type(HasTimeType::Time)]
1361
+
pubtraitUpdateLastModified<T> {
1362
+
fnupdate_last_modified(value:&mutT, time:Time);
1363
+
}
1364
+
1365
+
#[cgp_fn]
1366
+
#[uses(CurrentTime)]
1367
+
pubfntouch<T>(&self, value:&mutT) {
1368
+
lettime=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
+
1109
1376
### Contexts as explicit types
1110
1377
1111
1378
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