Here is the new Carbon Copy, your periodic update on the Carbon language!
Carbon Copy is designed for those people who want a high-level view of what's happening on the project. If you'd like to subscribe, you can join anno...@carbon-lang.dev. Carbon Copy should arrive roughly (sometimes extremely roughly) every other month.
In our last update, we showed interop, where Carbon called C++ seamlessly. Let's now take a look at C++ calling Carbon. (As we're all Carbon developers here, we call this "reverse interop".)
Here's our Carbon code for a vending machine base class:
class Snack {
var price: i64;
};
base class VendingMachine {
// A factory function to initialize the Carbon base class
fn Create(snack_price: i64) -> VendingMachine {
return {.snack_price = snack_price};
}
// Returns a Snack
fn DispenseItem[self: Self]() -> Snack {
// ... internal logic to retrieve a snack ...
return {.price = self.snack_price};
}
var snack_price: i64;
}
In C++, we can extend that base class and call a method that returns a pointer:
// C++ class extends the Carbon base class
class SnackMachine : public Carbon::VendingMachine {
public:
SnackMachine() : Carbon::VendingMachine(
Carbon::VendingMachine::Create(5)) {}
Carbon::Snack Vend() {
// Call a method inherited from the Carbon base class.
return DispenseItem();
}
};
int main() {
SnackMachine machine;
Carbon::Snack my_snack = machine.Vend();
std::cout << "Price = " << my_snack.price;
return 0;
}
See it in Compiler Explorer!
In this spotlight, we're going to take a look at generics again. (Will we run out of generics topics in Carbon? It is not this day.)
C++ provides mechanisms for specialization in templates. In this example of C++, we're going to create a template to handle crossing various kinds of legendary animals. Our first is the jackalope:
// THIS IS C++
#include <iostream>
struct Jackrabbit {};
struct Antelope {};
// Generic fallback for crossing any two animals
template <typename One, typename Two>
void Cross(One m, Two f) {
std::cout << "a generic legendary animal\n";
}
// "Partial specialization" via overloading: Any animal + Antelope
template <typename One>
void Cross(One m, Antelope f) {
std::cout << "something-antelope\n";
}
// Fully-specialized for Jackrabbit and Antelope
void Cross(Jackrabbit m, Antelope f) {
std::cout << "jackalope\n";
}
// THIS WAS STILL C++
C++ forbids the partial specialization of function templates, so we must use an overload. (Compiler explorer version.)
Switching over to Carbon, the approach fundamentally changes. In Carbon, you cannot directly specialize types or functions. Instead, Carbon relies on interfaces as its only static open extension mechanism. Specialization is done by delegating to interfaces and defining parameterized impl declarations that apply to various types.
To replicate the same logic in Carbon, and more, we start by defining an interface for the crossing operation.
import Cpp library "<iostream>";
private alias std = Cpp.std;
interface CrossableWith(U:! type) {
fn Cross[self: Self](other: U);
}
fn Cross [U:! type, T:! CrossableWith(U)] (t:T, u:U) {
t.Cross(u);
}
class Jackrabbit {}
class Antelope {}
// A blanket impl that applies to any two types
// anything->anything (makes a "legendary animal")
impl forall [T:! type, U:! type] T as CrossableWith(U) {
fn Cross[unused self: T](unused other: U) {
std.cout << "legendary animal\n";
}
}
Our generic looks about the same as C++, although we introduce a new keyword, forall.
The forall keyword is used to declare a parameterized impl (also known as a generic impl declaration). It introduces a list of compile-time generic parameters in square brackets immediately following the impl keyword, formatted as:
impl forall [ *generic parameters* ] *type* as *constraint*
Now that we've made a generic legendary animal cross, let's start specializing around jackrabbits.
// #1 jackrabbit->anything (get a jacka-something)
impl forall [U:! type] Jackrabbit as CrossableWith(U) {
fn Cross[unused self: Jackrabbit](unused other: U) {
std.cout << "jacka-something\n";
}
}
The next two impls for antelope/antelope and jackrabbit/antelope are fully-specified, so they are final. We could write the final keyword, but by fully-defining all the types (or having no parameters at all), they are effectively final, and function as final.
// A fully-specialized impl for antelope->antelope
impl Antelope as CrossableWith(Antelope) {
fn Cross[unused self: Antelope](unused other: Antelope) {
std.cout << "another antelope";
}
}
// A fully-specialized jackrabbit->antelope
impl Jackrabbit as CrossableWith(Antelope) {
fn Cross[unused self: Jackrabbit](unused other: Antelope) {
std.cout << "jackalope";
}
}
We have a problem here, one familiar to template writers worldwide. Were I to cross a jackrabbit and an antelope (or rather a Jackrabbit and an Antelope), what if more than one impl matches?
Carbon uses a strict, step-by-step selection algorithm when multiple impl declarations match a type and interface. The process of determining which specialization (or impl) to use is designed to guarantee implementation coherence, meaning the compiler must predictably choose the same implementation for a given query, regardless of which libraries are imported.
When crossing ancestors in our example, we have
a generic fallback
a non-specific Jackrabbit fallback, and
fully-specialized antelope and jackalope crosses.
When figuring out which specialization to use, there are three rules. Let's take a look at each one.
The first rule here is the final rule, not the last rule, if you follow my drift.
Rule 1, the final rule: Find a final implementation (or, as above, effectively final).
fn Run1() {
let one: Jackrabbit = {};
let two: Antelope = {};
// Output: "jackalope"
Cross(one, two);
}
In this case, the final rule does the work here. It chooses impl Jackalope as CrossableWith(Antelope).
Consider this case where we’re trying to create a skvader, the legendary Swedish hare-grouse:
// Same interface as above
// More animals to cross
class Hare {};
class Grouse {};
impl forall [U:! type] Hare as CrossableWith(U) {
fn Cross[unused self: Hare](unused other: U) {
std.cout << "hare-something\n";
}
}
impl forall [T:! type] T as CrossableWith(Grouse) {
fn Cross[unused self: T](unused other: Grouse) {
std.cout << "something-grouse\n";
}
}
fn Run2() {
let one: Hare = {};
let two: Grouse = {};
// Outputs "hare-something"
Cross(one, two);
}
In this case, with two non-final methods, the Overlap rule applies.
Rule 2: Overlap rule: If no final impls match, Carbon evaluates all matching non-final impls based on their "type structure", called the "overlap rule".
To determine the type structure, the compiler replaces all generic parameters in the impl with question marks (?).
Carbon then compares the type structures by reading them from left to right to find the first difference.
The impl that has a concrete type (a non-?) at that first difference is considered the most specific and wins.
For our example above, the (Hare ?) example would be chosen as compared to (?, Grouse), since (Hare ?) has the only non-? in the arguments going left-to-right. (Carbon explorer example.)
Sometimes, multiple impl declarations will have the exact same type structure (that is, they overlap without one being strictly more specific). If this happens, Carbon requires that all of those impl declarations be grouped together in the same match_first block (also called a prioritization block).
interface Horsey {}
interface Swimmy {}
// Just a regular Hippo, like you'd find in the Nile
class Hippo {}
// A Hippocampus is a legendary swimming horse hybrid (and, separately, a
// part of the brain)
class Hippocampus {}
impl Hippocampus as Horsey {}
impl Hippocampus as Swimmy {}
// Both of the following impls have the exact same type structure:
// `? as CrossableWith(Hippo)`.
// Because a Hippo satisfies both constraints, the compiler needs
// an explicit priority ranking.
match_first {
// Priority 1: Horsey animals crossed with a Hippo
impl forall [T:! Horsey] T as CrossableWith(Hippo) {
fn Cross[self: T](other: Hippo) {
std.cout << "Horsey-Hippo cross\n";
}
}
// Priority 2: Swimmy animals crossed with a Hippo
impl forall [T:! Swimmy] T as CrossableWith(Hippo) {
fn Cross[self: T](other: Hippo) {
std.cout <<"Swimmy-Hippo cross\n";
}
}
}
fn Run3() {
let one: Hippocampus = {};
let two: Hippo = {};
// Outputs "Horsey-Hippo cross"
Cross(one, two);
}
The Prioritization rule: When resolving the match, Carbon looks at this block and selects the first matching impl in the exact order they are listed. (Note: As of today, this is not implemented in the toolchain.)
You can see here we've prioritized Horsey vs Swimmy to match.
Specialization in Carbon only applies to templates, and when it does, there are three simple rules to remember for managing specialization ordering:
The final rule
The Overlap rule
The Prioritization rule
And that's it!
If you want to keep up on Carbon’s proposals and issues, follow "Last Week In Carbon". Check out the RSS feed!
Recently accepted proposals including:
Recently closed leads issues including:
Update to the syntax for class members: functions, methods, fields, etc. #6931
We should add ref bindings to Carbon, paralleling reference expressions #5261
Should we provide an idiomatic way to express Unix file permissions? #6821
Should we have a way to perform C++ default initialization? #6739
Don't forget to subscribe! You can join anno...@carbon-lang.dev. If you have comments or would like to contribute to future editions of Carbon Copy, please reach out. And, always, join us any way you can!
Next year in Andalusia,
Wolff, Josh, and the Carbon team