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
+271-3Lines changed: 271 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,281 @@ 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
+
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
+
pubtraitHasNameType {
1117
+
typeName: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
+
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 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:
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
+
pubtraitHasNameTypefor! {
1186
+
typeName: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
+
implGreetHello=Greetfor!
1194
+
where
1195
+
implNameType:HasNameType,
1196
+
with
1197
+
name:&NameType::Name,
1198
+
{
1199
+
fngreet() {
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
+
implUseFullNameType=HasNameType {
1211
+
typeName=FullName;
1212
+
}
1213
+
1214
+
fnmain() {
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
+
usestd::time::SystemTime;
1238
+
1239
+
pubtraitUpdateLastModified {
1240
+
fnupdate_last_modified(
1241
+
&mutself,
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
+
pubfntouch<T:UpdateLastModified>(value:&mutT) {
1251
+
lettime=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
+
pubtraitUpdateLastModified<Time> {
1262
+
fnupdate_last_modified(
1263
+
&mutself,
1264
+
time:Time,
1265
+
);
1266
+
}
1267
+
1268
+
pubfntouch<Time, T:UpdateLastModified<Time>>(
1269
+
value:&mutT,
1270
+
)
1271
+
where
1272
+
Time:From<SystemTime>,
1273
+
{
1274
+
lettime=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
+
pubCurrentTime {
1285
+
fncurrent_time() ->Self;
1286
+
}
1287
+
1288
+
pubtraitUpdateLastModified {
1289
+
typeTime:CurrentTime;
1290
+
1291
+
fnupdate_last_modified(
1292
+
&self,
1293
+
time:Self::Time,
1294
+
);
1295
+
}
1296
+
1297
+
pubfntouch<T:UpdateLastModified>(value:&mutT) {
1298
+
lettime=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
+
pubtraitHasTimeTypefor! {
1311
+
typeTime;
1312
+
}
1313
+
1314
+
// The current time is provided by with ambient context using a zero-arity trait
1315
+
pubtraitCurrentTimefor!
1316
+
where
1317
+
implTimeImpl:HasTimeType,
1318
+
{
1319
+
fncurrent_time() ->TimeImpl::Time
1320
+
}
1321
+
1322
+
// The `Time` type does not depend on `Self`
1323
+
pubtraitUpdateLastModified
1324
+
where
1325
+
implTimeImpl:HasTimeType,
1326
+
{
1327
+
fnupdate_last_modified(
1328
+
&mutself,
1329
+
time:TimeImpl::Time,
1330
+
);
1331
+
}
1332
+
1333
+
// The time type and current time implementations are not bound to `T`
1334
+
pubincoherentfntouch<T:UpdateLastModified>(
1335
+
value:&mutT,
1336
+
)
1337
+
where
1338
+
implTimeImpl:HasTimeType+CurrentTime,
1339
+
{
1340
+
lettime=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
+
pubtraitHasTimeType {
1352
+
typeTime;
1353
+
}
1354
+
1355
+
#[cgp_component(CurrentTimeImpl)]
1356
+
pubtraitCurrentTime:HasTimeType {
1357
+
fncurrent_time(&self) ->Self::Time;
1358
+
}
1359
+
1360
+
#[cgp_component(UpdateLastModifiedImpl)]
1361
+
#[use_type(HasTimeType::Time)]
1362
+
pubtraitUpdateLastModified<T> {
1363
+
fnupdate_last_modified(value:&mutT, time:Time);
1364
+
}
1365
+
1366
+
#[cgp_fn]
1367
+
#[uses(CurrentTime)]
1368
+
pubfntouch<T>(&self, value:&mutT) {
1369
+
lettime=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
+
1109
1377
### Contexts as explicit types
1110
1378
1111
1379
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