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 very roughly) every other month.
We've made a lot of progress on generics in the toolchain, which now supports generic classes and interfaces. This is being used to implement integers, such as i32, as a generic library type parameterized by the bitwidth, Core.Int(N), following our "All APIs are library APIs" principle.
Earlier this year, we noted some ambitions (full talk) for our compile-time performance, and we've been making steady progress. This includes developing a robust compile-time benchmarking framework, and writing an optimized hash table and hashing function. The hash tables are used for keeping track of the identifiers encountered while lexing the input source code. Using our benchmarking infrastructure, we get these results on an Apple M1:
We also have an exciting Carbon prototype solution to the Advent of Code day one challenge! It is far from what we expect long term, requiring lots of workarounds and stubbing out capabilities. Right now Advent of Code challenges are still more useful for driving toolchain improvements than for folks to learn about Carbon, but stay tuned for next year!
This issue we're going to talk about structs in Carbon. Structs are lightweight types that are used for when you need more than a tuple but less than a class. It allows you to name the members of your tuples and refer to them in arbitrary order.
(If you haven't read about patterns and tuples yet, see the Spotlight in Carbon Copy #4! As we look at structs, the syntax should look pretty familiar.)
A struct is just a list of data member names and types. Struct and struct values have a literal syntax written using curly braces {...}, allowing you to write them in code. They both name their fields using _designators_, written with a period (.) followed by the name of the field. The difference between them is that struct values follow the designator with "= value" while struct types use ": type".
// The basic case, which takes up 0 memory.
var empty_struct_instance: {} = {};
// Add some fields, name them, and initialize
var point0: {.x: f64, .y: f64} = {.x = 2.0, .y = 3.0};
var point1: {.x: f64, .y: f64} = {.x = 3.0, .y = 5.0};
// Access those initialized fields
var slope: f64 = (point1.y - point0.y) /
(point1.x - point0.x);
We've used the term fields here to refer to a data member that has typed storage within each instance. (Classes, too, can have data members, but they also have functions, methods, associated constants, and so on.)
An intended use case of struct values is to write out examples in source code that may be in a test.
fn TestTransfers() {
var accounts: AccountDatabase = CreateTestDatabase(
({.account = 3, .balance = 12},
{.account = 6, .balance = 100},
{.account = 8, .balance = 4}));
CheckSuccess(accounts.Transfer(
{.from_account = 3, .to_account = 8, .amount = 10}));
// After the first transfer, account 3 will have a
// balance of 2, so this second transfer will fail.
CheckFailure(accounts.Transfer(
{.from_account = 3, .to_account = 8, .amount = 10}));
CheckSuccess(accounts.Transfer(
{.from_account = 8, .to_account = 6, .amount = 5}));
Assert(accounts.Dump() ==
({.account = 3, .balance = 2},
{.account = 6, .balance = 105},
{.account = 8, .balance = 9}));
}
In C++, if you want to have an object that contains multiple values, you have to pre-declare the type and give it a name, but with structs you can just write something out in the code.
Struct values are also convenient for return values.
// A set of 64-bit integers, stored in an array kept
// in sorted order.
class SortedArray {
// `Insert` only adds `value` if it is not already
// in the set. It returns a struct with whether it
// was added and what position `value` is after
// the call.
fn Insert[addr self: Self*](value: i64)
-> {.inserted: bool, .position: i32};
// ...
}
fn TestSortedArray() {
var s: SortedArray = SortedArray.Make();
var result: auto = s.Insert(5);
Assert(result.inserted == true);
Assert(result.position == 0);
result = s.Insert(7);
Assert(result ==
{.inserted = true, .position = 1});
result = s.Insert(5);
Assert(result ==
{.inserted = false, .position = 0});
}
When not marked as references, struct values are passed by "most-convenient" in function calls and returns, which means they may be references, copies, registers, or something else as best determined by the compiler. (From the code's perspective, you can't tell the difference!)
It's worth reminding ourselves here that structs are types, which means they are checked at compile time. This means that you can't return a struct with an arbitrary number of fields or null; you must return exactly the type specified.
// ❌ Don't do this!
fn ValidatePoint(input: {x: f32, y: f32}) -> auto {
if !ValidatePoint(input) {
// The compiler believes this function returns type {}
return {};
}
// ❌ Compile error: incompatible return type
return input;
}
When initializing a struct value, you use =. Initialization is in the left-side order, even if you write them in some other order on the right.
var s: {.a: i32, .b: i32};
// ❌ Compile error: field names don't match
s = {.a = 1, .b = 2, .c = 3};
s = {.a = 1};
s = {.a = 1, .c = 3};
// ❌ Compile error: types incompatible
s = {.a = true, .b = "string"};
// ✅ Allowed: field names can be in any order.
// Assigns `s.a` first, and then `s.b`.
s = {.b = 3, .a = 2};
// ✅ Allowed: can implicitly convert between
// field types.
s = {.a = 4 as i8, .b = 5 as i16};
When comparing structs, equality is field-wise. The comparisons happen in the order of left side struct's fields, with short-circuiting. So, this:
if ({.i = 2, .s = "Very long string, with only the end different"} ==
{.s = "Very long string, with only the end changed", .i = 3}) {
// ...
}
is equivalent to:
let lhs: auto = {.i = 2, .s = "Very long string, with only the end different"};
let rhs: auto = {.s = "Very long string, with only the end changed", .i = 3});
if (lhs.i == rhs.i and lhs.s == rhs.s) {
// lhs.i != rhs.i, so lhs.s never compared to rhs.s
}
Inequalities are only permitted if fields are in the same order.
let a: {.x: i32, .y: i32} = {.x = 1, .y = 2};
let b: {.x: i32, .y: i32} = {.x = 2, .y = 1};
Assert(a < b);
// equivalent to:
// Assert(a.x < b.x or (a.x == b.x and a.y < b.y));
// ❌ Compile error: can't compare two structs
// unless their fields are in the same order.
if (a < {.y = 2, .x = 2}) { ... }
A useful property is that a = b results in a == b, even when a and b have different field orders.
The fields of a struct are laid out in memory in the same order as they are written in the source. (Due to padding, this may mean that the struct takes up more memory than a different order.) The Carbon language favors such predictability, which gives the developer control and allows the compiler to be simpler and more transparent.
Like var/let from Carbon Copy #3, you can use var to create objects which have addresses and are mutable. Without var, they are values which are only readable, and have no fixed location. [Footnote 1]
let a: {.x: i32, .y: i32} = {.x = 1, .y = 2};
var b: {.x: i32, .y: i32} = {.x = 2, .y = 1};
// ❌ Compile error: can't mutate a value
a = b;
// This is OK
b = a;
Remember that values are much more flexible for the compiler; it can choose where to store it, and figure out what lifespan it needs. (Read more on Carbon Copy #2!)
You can mix and match variables, values, and pointers, as struct definitions are also patterns like function definitions.
let map_location_val:
{.x: i32, .y: i32, .payload: AClassType} =
{.x = 5, .y = 5, .payload = AClassType.Make()};
var map_location_obj:
{.x: i32, .y: i32, .payload: AClassType};
// Pointer instead: Now we should be managing the
// memory for .payload follows
var map_location_w_ptr:
{.x: i32, .y: i32, .payload: AClassType*} =
{5, 5, &some_object_defined_earlier};
// Pointer to a struct value
var map_location_w_ptr_ptr:
{.x: i32, .y: i32, .payload: AClassType*}* =
&map_location_w_ptr;
It's reasonable and expected to use a struct to initialize an instance of a class. This takes advantage of the fact that the class and the struct literal have the same field names.
class AnInitializeablePoint {
var x: f64;
var y: f64;
}
var ne_point: AnInitializeablePoint = {.x = 1, .y = -1};
var sw_point: AnInitializeablePoint = {.x = -1, .y = 1};
As we said at the start, structs and struct values are there when you need more than a tuple but less than a class. They have predictable structure and simple memory layout, and conform syntactically to the patterns we have seen elsewhere in Carbon. They are workhorses for in-code literals, and they are fantastic for passing data around, both in argument lists and return values.
(Did you notice that we never needed a struct keyword?)
One question you might ask after reading all this: Is a struct a class? Sort of. A struct is an anonymous data class, which means it is a container that has named data in it. A data class is the basis for a class (which contains named data, too), but an actual class would be named, can have methods, and wouldn't be usable in the same way.
Speaking of classes, in the next Spotlight we'll talk all about them!
Approved & merged proposals since last newsletter:
New leads questions:
When are interface requirements enforced for an impl? #4579
What has access to private and protected members? #4575
Implementing multiple interfaces with a single impl definition #4566
Should classes explicitly declare whether they have a vptr? #4488
Can you adapt an abstract class? #4387
Recent
"The Carbon Language: Road to 0.1", NDC {TechTown} (Slides, video forthcoming)
"Generic implementation strategies in Carbon and Clang", LLVM Developers' Meeting (video, slides)
How designing Carbon with C++ interop taught me about C++ variadics and overloads, CppNorth (video, slides)
See all talks here.
If you want more about the current discussion, check out the weekly meeting notes from the Carbon Weekly Sync.
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!
Allotropically yours,
Josh, Wolff, and the Carbon team
Footnote
[Footnote 1] Writing that out makes values sound sad and lonely. They aren't! They are loved (well, they are "valued") for as long as they are needed.