The Why and the How (Part 1) (original) (raw)

Definition Checked Generics

Chandler Carruth

Josh Levenberg
Richard Smith

CppNow 2023

The why of checked generics

What are checked generics?

or

Let’s start with C++20 constrained templates

C++20 constrained templates use concepts

Let’s try to definition check with these

template<typename D>
concept Display = requires(D &d, std::string_view sv) {
  `<1>d.Show(sv)`;
};

template<Display D> void hello(D &d) {
  `<2>d.Show("Hello, world!"sv)`;
}
template<typename D>
concept Display = requires(D &d, std::string_view sv) {
  d.Show(sv);
};

template<Display D> void hello(D &d, std::string name = "world") {
  d.Show("Hello, " + name + "!");
}

struct FormattedText {
  FormattedText(std::string_view);
};
struct MyDisplay {
  void Show(FormattedText text);
};
void test(MyDisplay &d, std::string_view sv) {
  // ✅: This is fine, so concept is satisfied!
  d.Show(sv);
  // ❌: This doesn't work though!
  hello(d);
}
template<typename T> struct ConvertsTo {
  operator T();
};
template<typename D>
concept Display = requires(D &d, std::string_view sv1,
                           ConvertsTo<std::string_view> sv2) {
  `<1>d.Show(sv1)`;
  `<0>d.Show(sv2)`;
};
template<typename T> struct ConvertsTo {
  operator T();
};
template<typename D>
concept Display = requires(D &d, std::string_view sv1,
                           ConvertsTo<std::string_view> sv2,
                           `<2>const std::string_view sv3`) {
  d.Show(sv1);
  d.Show(sv2);
  `<1>d.Show(std::move(sv1))`;
  `<2>d.Show(sv3)`;
  `<3>d.Show(std::move(sv3))`;
};
`<4>int` `<3>ScaleTime`(int time);
double ScaleTime(float time);
double ScaleTime(double time);
void RecordTime(`<5>double &time`);


template<Display D> void hello(D &d, std::string name = "world") {
  `<4>auto` time = `<3>ScaleTime`(d.Show("Hello, " + name + "!"));
  RecordTime(`<5>time`);
}

struct BadDisplay {
  `<2>double` Show(std::string_view);

  // Custom version.
  `<2>int` Show(`<1>std::string`);
};

Definition checking C++20 concepts is infeasible, not impossible

😞

Why is type checking generic definitions useful?

Better error messages?

https://twitter.com/mcclure111/status/1079068076560343041

Better error messages?

Example from the original Concepts Lite paper:

list<int> lst = ...;
sort(lst); // Error
error: no matching function for call to ‘sort(list<int>&)’
   sort(l);
         ^
note: candidate is:
note: template<Sortable T> void sort(T)
   void sort(T t) { }
        ^
note: template constraints not satisfied because
note:   ‘T’ is not a/an ‘Sortable’ type [with T = list<int>] since
note:     ‘declval<T>()[n]’ is not valid syntax

Better error messages?

Mostly covered by C++20 concepts

Lots more to do on error messages,

but definition checking isn’t crucial there

Definition checking helps you get the errors

Changes how to develop generic code

Is static typing useful?

IMO, yes: shifting-left & large-scale refactoring

Checked generics give static typing benefits

for large-scale generic software.

Complete definition checking unlocks type erasure

Type erasure is a powerful missing abstractions

Type-checked definitions also improve implementation options

Checked generics can also improve the foundations of the language

What do checked generics look like in practice?

Generic means “parameterized”

For comparison, what do template generics with C++20 concepts look like?

C++ example: defining a concept

#include <concepts>

template<typename T>
concept `RNGConcept` = requires(T a) {
    { `a.random()` } -> std::same_as<typename `T::result_t`>;
};

class BaseRNGClass { ... };

class FancyRNG : public BaseRNGClass {
 public:
  typedef double result_t;
  auto random() -> double { ... }
};

template<RNGConcept T>
auto GenericFunction(T r) -> T::result_t {
  return r.random();
}

auto CallsGeneric(FancyRNG r) -> double {
  return GenericFunction(r);
}

C++ example: a type implementing the concept

#include <concepts>

template<typename T>
concept RNGConcept = requires(T a) {
    { a.random() } -> std::same_as<typename T::result_t>;
};

class BaseRNGClass { ... };

class FancyRNG : public BaseRNGClass {
 public:
  `<1>typedef double result_t`;
  auto `<0>random() -> double` { ... }
};

template<RNGConcept T>
auto GenericFunction(T r) -> T::result_t {
  return r.random();
}

auto CallsGeneric(FancyRNG r) -> double {
  return GenericFunction(r);
}

C++ example: a generic function

#include <concepts>

template<typename T>
concept RNGConcept = requires(T a) {
    { a.random() } -> std::same_as<typename T::result_t>;
};

class BaseRNGClass { ... };

class FancyRNG : public BaseRNGClass {
 public:
  typedef double result_t;
  auto random() -> double { ... }
};

template<`<1>RNGConcept` T>
auto `<0>GenericFunction(T r)` -> T::result_t {
  return r.random();
}

auto CallsGeneric(FancyRNG r) -> double {
  return GenericFunction(r);
}

C++ example: calling a generic function

#include <concepts>

template<typename T>
concept RNGConcept = requires(T a) {
    { a.random() } -> std::same_as<typename T::result_t>;
};

class BaseRNGClass { ... };

class FancyRNG : public BaseRNGClass {
 public:
  typedef double result_t;
  auto random() -> double { ... }
};

template<RNGConcept T>
auto GenericFunction(T r) -> T::result_t {
  return r.random();
}

auto CallsGeneric(FancyRNG r) -> double {
  return GenericFunction(r);
}

Languages with checked generics are going to have similar facilities

Generic functions

template<RNGConcept `<0>T`>
auto GenericFunction(`<0>T` r) -> `<0>T`::result_t {
  return r.random();
}

Generic types

Checked generic means the parameters are constrained

template<`RNGConcept` T>
auto GenericFunction(T r) -> T::result_t {
  return r.random();
}

Interfaces

The building blocks of constraints

C++ Swift Rust Carbon
C++20 concept protocol trait interface
template<typename T>
concept RNGConcept = requires(T a) {
    { a.random() } -> std::same_as<typename T::result_t>;
};

Structural interfaces

If you have these methods, with these signatures, then you satisfy this interface

Nominal interfaces

There is an explicit statement – by name – that a type satisfies a requirement

Associated types

#include <concepts>

template<typename T>
concept RNGConcept = requires(T a) {
    { a.random() } -> std::same_as<typename `<0>T::result_t`>;
};

class BaseRNGClass { ... };

class FancyRNG : public BaseRNGClass {
 public:
  typedef double `<0>result_t`;
  auto random() -> double { ... }
};

Associated types

Generic interfaces

Some languages allow interfaces to be parameterized as well

template<typename T, `typename U`>
concept Pow = requires(T a, U b) {
    { a.pow(b) } -> std::same_as<typename T::result_t>;
};

template<Pow`<int>` T>
auto GenericFunction(T r) -> T::result_t {
  return r.pow(2);
}

Generic interfaces

Hold on, there were two inputs

template<`<0>typename T`, typename U>
concept Pow = requires(`<0>T a`, U b) {
    { a.pow(b) } -> std::same_as<typename T::result_t>;
};

Generic implementations

What do checked generics look like?

Reminder: C++20 concepts

C++20 concepts are only constraints on the caller

C++20 concepts are generally structural

However subsumption is nominal

C++ example: defining a concept

#include <concepts>

template<typename T>
concept RNGConcept = requires(T a) {
    { a.random() } -> std::same_as<typename T::result_t>;
};

class BaseRNGClass { ... };

class FancyRNG : public BaseRNGClass {
 public:
  typedef double result_t;
  auto random() -> double { ... }
};

template<RNGConcept T>
auto GenericFunction(T r) -> T::result_t {
  return r.random();
}

C++ example: a type implementing the concept

#include <concepts>

template<typename T>
concept RNGConcept = requires(T a) {
    { a.random() } -> std::same_as<typename T::result_t>;
};

class BaseRNGClass { ... };

class FancyRNG `<1>: public BaseRNGClass` {
 public:
  typedef double result_t;
  `<0>auto random() -> double` { ... }
};

template<RNGConcept T>
auto GenericFunction(T r) -> T::result_t {
  return r.random();
}

C++ example: a generic function

#include <concepts>

template<typename T>
concept RNGConcept = requires(T a) {
    { a.random() } -> std::same_as<typename T::result_t>;
};

class BaseRNGClass { ... };

class FancyRNG : public BaseRNGClass {
 public:
  typedef double result_t;
  auto random() -> double { ... }
};

template<`RNGConcept` `T`>
auto GenericFunction(`T r`) -> T::result_t {
  return r.random();
}

Swift

Swift example: defining a protocol

protocol RNGProtocol {
  `associatedtype Result`
  `mutating func random() -> Result`
}

class BaseRNGClass { ... }

class FancyRNG: BaseRNGClass, RNGProtocol {
  func random() -> Double { ... }
}

func GenericFunction<T: RNGProtocol>(_ r: inout T) -> T.Result {
  return r.random()
}

Swift example: a type conforming to a protocol

protocol `<0>RNGProtocol` {
  associatedtype Result
  mutating func `<2>random`() -> `<3>Result`
}

class `<1>BaseRNGClass` { ... }

class FancyRNG: `<1>BaseRNGClass`, `<0>RNGProtocol` {
  func `<2>random`() -> `<3>Double` { ... }
}

func GenericFunction<T: RNGProtocol>(_ r: inout T) -> T.Result {
  return r.random()
}

Swift example: a generic function

protocol RNGProtocol {
  associatedtype Result
  mutating func random() -> Result
}

class BaseRNGClass { ... }

class FancyRNG: BaseRNGClass, RNGProtocol {
  func random() -> Double { ... }
}

func GenericFunction<`T`: `RNGProtocol`>(_ r: inout T) -> `T.Result` {
  return r.random()
}

Some things Swift does not (yet) do

Rust

Rust example: defining a trait

pub trait RNGTrait {
  `type Result;`
  `fn random(&mut self) -> Self::Result;`
}

pub struct BaseRNG { ... }

pub struct FancyRNG {
  base: BaseRNG,  // no inheritance
}
impl RNGTrait for FancyRNG {
  type Result = f64;
  fn random(&mut self) -> f64 { ... }
}

fn generic_function<T: RNGTrait>(r: &mut T) -> T::Result {
  return r.random();
}

Rust example: a type implementing to a trait

pub trait RNGTrait {
  type Result;
  fn random(&mut self) -> Self::Result;
}

pub struct BaseRNG { ... }

pub struct FancyRNG {
  base: BaseRNG,  // no inheritance
}
`impl RNGTrait for FancyRNG` {
  type Result = f64;
  `fn random(&mut self) -> f64` { ... }
}

fn generic_function<T: RNGTrait>(r: &mut T) -> T::Result {
  return r.random();
}

Rust example: a generic function

pub trait RNGTrait {
  type Result;
  fn random(&mut self) -> Self::Result;
}

pub struct BaseRNG { ... }

pub struct FancyRNG {
  base: BaseRNG,  // no inheritance
}
impl RNGTrait for FancyRNG {
  type Result = f64;
  fn random(&mut self) -> f64 { ... }
}

fn generic_function<`T: RNGTrait`>(r: &mut T) -> `T::Result` {
  return r.random();
}

Rust has been adding some advanced features

Recent releases have added support for:

Some things Rust does not do

Carbon

Carbon example: defining an interface

interface RNGInterface {
  `let Result: type;`
  `fn Random[addr self: Self*]() -> Result;`
}

class BaseRNGClass { ... }

class FancyRNG {
  extend base: BaseRNGClass;
  extend impl as RNGInterface where .Result = f64 {
    fn Random[addr self: Self*]() -> f64 { ... }
  }
}

fn GenericFunction[T:! RNGInterface](r: T*) -> T.Result {
  return r->Random();
}

Carbon example: implementing an interface

interface RNGInterface {
  let Result: type;
  fn Random[addr self: Self*]() -> Result;
}

class BaseRNGClass { ... }

class FancyRNG {
  `<1>extend` `<2>base: BaseRNGClass`;
  `<1>extend` `<0>impl as RNGInterface` where .Result = f64 {
    `<3>fn Random[addr self: Self*]() -> f64` { ... }
  }
}

fn GenericFunction[T:! RNGInterface](r: T*) -> T.Result {
  return r->Random();
}

Carbon example: generic function

interface RNGInterface {
  let Result: type;
  fn Random[addr self: Self*]() -> Result;
}

class BaseRNGClass { ... }

class FancyRNG {
  extend base: BaseRNGClass;
  extend impl as RNGInterface where .Result = f64 {
    fn Random[addr self: Self*]() -> f64 { ... }
  }
}

fn GenericFunction`[T:! RNGInterface]``(r: T*)` -> T.Result {
  return r->Random();
}

Carbon example: generic function

interface RNGInterface {
  let Result: type;
  fn Random[addr self: Self*]() -> Result;
}

class BaseRNGClass { ... }

class FancyRNG {
  extend base: BaseRNGClass;
  extend impl as RNGInterface where .Result = f64 {
    fn Random[addr self: Self*]() -> f64 { ... }
  }
}

fn GenericFunction[T`:!` RNGInterface](r: T*) -> `T.Result` {
  return r->Random();
}

Carbon

Better language foundations with checked generics

Unified and powerful customization points

What are customization points?

class MyComplex { ... };

MyComplex `operator+`(MyComplex, MyComplex) { ... }
void `swap`(MyComplex, MyComplex) { ... }

void f(std::vector<MyComplex> vec) {
  // Uses ``operator+`` customization point.
  MyComplex avg = `std::accumulate`(vec.begin(), vec.end(),
                                  MyComplex{})
                  / vec.size();

  // Uses ``swap`` customization point.
  `std::partial_sort`(vec.begin(), vec.end(),
                    [&](MyComplex c) {
                      return c.real() < avg.real();
                    });
}

Long, complex history trying to get this right

Many WG21 papers here, but can start with: http://wg21.link/p2279

Checked generics solve these problems

Operator overloading

interface `MulWith`(`U:! type`) {
  `let Result:! type` `= Self`;
  fn `Op`[self: Self](rhs: U) -> Result;
}

class Point {
  var x: f64;
  var y: f64;

  impl as MulWith(f64) where .Result = Point {
    fn Op[self: Self](scale: f64) -> Point {
      return {.x = self.x * scale, .y = self.y * scale};
    }
  }
}

fn Double(p: Point) -> auto {
  let scale: f64 = 2.0;
  return p * scale;
  // => p.(MulWith(typeof(scale)).Op)(scale)
  // => p.(MulWith(f64).Op)(scale)
}

Operator overloading

interface MulWith(U:! type) {
  let Result:! type = Self;
  fn Op[self: Self](rhs: U) -> Result;
}

class `Point` {
  var x: f64;
  var y: f64;

  `impl as MulWith(f64)` where .Result = Point {
    fn Op[self: Self](scale: f64) -> Point {
      return {.x = self.x * scale, .y = self.y * scale};
    }
  }
}

fn Double(p: Point) -> auto {
  let scale: f64 = 2.0;
  return p * scale;
  // => p.(MulWith(typeof(scale)).Op)(scale)
  // => p.(MulWith(f64).Op)(scale)
}

Operator overloading

interface MulWith(U:! type) {
  let Result:! type = Self;
  fn Op[self: Self](rhs: U) -> Result;
}

class Point {
  var x: f64;
  var y: f64;

  impl as MulWith(f64) `where .Result = Point` {
    fn Op[self: Self](scale: f64) -> Point {
      `return {.x = self.x * scale, .y = self.y * scale};`
    }
  }
}

fn Double(p: Point) -> auto {
  let scale: f64 = 2.0;
  return p * scale;
  // => p.(MulWith(typeof(scale)).Op)(scale)
  // => p.(MulWith(f64).Op)(scale)
}

Operator overloading

interface MulWith(U:! type) {
  let Result:! type = Self;
  fn Op[self: Self](rhs: U) -> Result;
}

class Point {
  var x: f64;
  var y: f64;

  impl as MulWith(f64) where .Result = Point {
    fn Op[self: Self](scale: f64) -> Point {
      return {.x = self.x * scale, .y = self.y * scale};
    }
  }
}

fn Double(p: Point) -> auto {
  let scale: f64 = 2.0;
  return `p * scale`;
  // => `p.(MulWith(typeof(scale)).Op)(scale)`
  // => p.(MulWith(f64).Op)(scale)
}

Operator overloading

interface MulWith(U:! type) {
  let Result:! type = Self;
  fn Op[self: Self](rhs: U) -> Result;
}

class Point {
  var x: f64;
  var y: f64;

  impl as MulWith(f64) where .Result = `<5>Point` {
    fn `<2>Op`[self: Self](scale: f64) -> Point {
      return {.x = self.x * scale, .y = self.y * scale};
    }
  }
}

fn Double(p: Point) -> `<5>auto` {
  let scale: f64 = 2.0;
  return p * `<1>scale`;
  // => p.(MulWith(`<1>typeof(scale)`).Op)(scale)
  // => `<3>p`.(`<2>MulWith(f64).Op`)(`<4>scale`)
}

Customizations with higher-level semantics

choice Ordering {
  Less,
  Equivalent,
  Greater,
  Incomparable
}

interface OrderedWith(U:! type) {
  fn Compare[self: Self](u: U) -> Ordering;
}

fn StringLess(s1: String, s2: String) -> bool {
  return s1 < s2;
  // => s1.(OrderedWith(String).Compare)(s2) == Less
}

fn StringGreater(s1: String, s2: String) -> bool {
  return s1 > s2;
  // => s1.(OrderedWith(String).Compare)(s2) == Greater
}

Customizations with higher-level semantics

choice Ordering {
  Less,
  Equivalent,
  Greater,
  Incomparable
}

interface OrderedWith(U:! type) {
  fn Compare[self: Self](u: U) -> Ordering;
}

fn StringLess(s1: String, s2: String) -> bool {
  return s1 `<2><` s2;
  // => `<1>s1.(OrderedWith(String).Compare)(s2)` `<2>== Less`
}

fn StringGreater(s1: String, s2: String) -> bool {
  return s1 `<3>>` s2;
  // => `<1>s1.(OrderedWith(String).Compare)(s2)` `<3>== Greater`
}

Note: Carbon actually supports deeper customization, motivated by C++ interop

Incrementally extending & specializing customization points

interface OrderedWith(U:! type) {
  fn Compare[self: Self](u: U) -> Ordering;

  default fn Less[self: Self](u: U) -> bool {
    return self.Compare(u) == Ordering.Less;
  }
  default fn LessOrEquivalent[self: Self](u: U) -> bool {
    let c: Ordering = self.Compare(u);
    return c == Ordering.Less or c == Ordering.Equivalent;
  }

  default fn Greater[self: Self](u: U) -> bool {
    return self.Compare(u) == Ordering.Greater;
  }
  default fn GreaterOrEquivalent[self: Self](u: U) -> bool {
    let c: Ordering = self.Compare(u);
    return c == Ordering.Greater or c == Ordering.Equivalent;
  }
}

Incrementally extending & specializing customization points

interface OrderedWith(U:! type) {
  fn Compare[self: Self](u: U) -> Ordering;

  `<2>default` fn `<1>Less`[self: Self](u: U) -> bool {
    `<3>return self.Compare(u) == Ordering.Less;`
  }
  default fn LessOrEquivalent[self: Self](u: U) -> bool {
    let c: Ordering = self.Compare(u);
    return c == Ordering.Less or c == Ordering.Equivalent;
  }

  default fn Greater[self: Self](u: U) -> bool {
    return self.Compare(u) == Ordering.Greater;
  }
  default fn GreaterOrEquivalent[self: Self](u: U) -> bool {
    let c: Ordering = self.Compare(u);
    return c == Ordering.Greater or c == Ordering.Equivalent;
  }
}

Conditional, generic customization points

interface `Printable` {
  fn `Print`[self: Self]();
}

class `Vector(template T:! type)` { ... }

impl `forall` [`T`:! `Printable`] `Vector(T)` as Printable {
  fn Print[self: Self]() {
    var first: bool = true;
    for (elem: `T` in self) {
      if (not first) { ", ".Print(); }
      `elem.Print()`;
      first = false;
    }
  }
}

Implicit conversions with customization points

Explicit conversion customization point

interface `As`(`Dest:! type`) {
  fn `Convert`[self: Self]() -> `Dest`;
}

`impl String as As`(`Path`) {
  fn Convert[self: String]() -> Path {
    return `Path.FromString`(self);
  }
}

let config_file: Path = `"/etc/myutil.cfg" as Path`;
//                      => ("/etc/myutil.cfg").(`As(Path)`.`Convert`)()

Implicit conversion customization point

interface `ImplicitAs`(Dest:! type) {
  `extends As(Dest)`;
  // Inherited from As(Dest):
  // fn `Convert`[self: Self]() -> Dest;
}

impl `String as ImplicitAs(StringView)` {
  fn Convert[self: String]() -> StringView {
    return StringView::Make(self.Data(), self.Data() + self.Size());
  }
}

fn Greet(s: StringView) { Print("Hello, {0}", s); }

fn Main() -> i32 {
  `Greet`(`"audience"`);
  // => Greet(("audience").(`ImplicitAs(StringView)`.`Convert`)()
  return 0;
}

Implicit conversion conditional defaults

impl `forall` [`U:! type`, `T:! As(U)`]
     `Optional(T)` as `As(Optional(U))`;

impl forall [U:! type, T:! `ImplicitAs(U)`]
     Optional(T) as ImplicitAs(Optional(U));

impl forall [T:! type]
     `NullOpt` as `ImplicitAs(Optional(T))`;

Fundamentally more expressive customization

This works! ✅

class `Base` {};
class `Derived` : public Base {};

void Test(`Base *b`);

void Example(bool condition) {
  `Base b`;
  `Derived d`;


  // ✅
  Test(`condition ? &b : &d`);


  //...
}

This works in either direction! ✅

class Base {};
class Derived : public Base {};

void Test(Base *b);

void Example(bool condition) {
  Base b;
  Derived d;


  // ✅✅
  Test(condition ? &b : &d);
  Test(`condition ? &d : &b`);


  //...
}

But does this? 😞

class Base {};
class `DerivedA` : public Base {};
class `DerivedB` : public Base {};
void Test(Base *b);

void Example(bool condition) {
  Base b;
  `DerivedA da`;
  `DerivedB db`;

  // ✅✅
  Test(condition ? &b : &db);
  Test(condition ? &da : &b);

  // ???
  Test(`condition ? &da : &db`);

  //...
}

❌ error: incompatible operand types (DerivedA * and DerivedB *)

We can make this easy in Carbon

interface `CommonTypeWith`(`U:! type`) {
  `let Result:! type`
    `where` `Self impls ImplicitAs`(`.Self`) and
          `U impls ImplicitAs`(`.Self`);
}

class `InternedString` { ... }
impl `InternedString` as `CommonTypeWith(String)`
  where `.Result = StringView` {}

fn SelectString(condition: bool, s: String, i: InternedString) -> StringView {
  // Carbon version of ``... ? ... : ...`` in C++:
  return `if condition then s else i`;
}

Customizable CommonType opens even more doors

fn SelectLongString(s: String, i: InternedString, v: StringView) -> `auto` {
  if (s.Size() > 20) {
    `return s`;
  } else if (i.Size() > 20) {
    `return i`;
  } else {
    `return v`;
  }
}

Checked generics build better language foundations

These better foundations make generics better!

Foundations built with checked generics

become available within checked generics

Operator overloads in checked generic code

interface MulWith(U:! type) {
  let Result:! type = Self;
  fn Op[self: Self](rhs: U) -> Result;
}

class Point {
  var x: f64;
  var y: f64;
  impl as MulWith(f64) where .Result = Point {
    fn Op[self: Self](scale: f64) -> Point;
  }
}

fn Double(p: Point) -> auto {
  let scale: f64 = 2.0;
  return p * scale;
  // => p.(MulWith(f64).Op)(scale)
}

fn GenericDouble[T:! MulWith(f64)](x: T) -> auto {
  let scale: f64 = 2.0;
  return x * scale;
  // => p.(MulWith(f64).Op)(scale)
}

Operator overloads in checked generic code

interface MulWith(U:! type) {
  let Result:! type = Self;
  fn Op[self: Self](rhs: U) -> Result;
}

class Point {
  var x: f64;
  var y: f64;
  impl as MulWith(f64) where .Result = Point {
    fn Op[self: Self](scale: f64) -> Point;
  }
}

fn Double(p: Point) -> auto {
  let scale: f64 = 2.0;
  return p * scale;
  // => p.(MulWith(f64).Op)(scale)
}

fn GenericDouble[`T:! MulWith(f64)`](`x: T`) -> auto {
  let scale: f64 = 2.0;
  return `x * scale`;
  // => p.(MulWith(f64).Op)(scale)
}

Operator overloads in checked generic code

interface MulWith(U:! type) {
  let `<5>Result`:! type = Self;
  fn Op[self: Self](rhs: U) -> `<4>Result`;
}

class Point {
  var x: f64;
  var y: f64;
  impl as MulWith(f64) where .Result = Point {
    fn Op[self: Self](scale: f64) -> Point;
  }
}

fn Double(p: Point) -> auto {
  let scale: f64 = 2.0;
  return p * scale;
  // => p.(MulWith(f64).Op)(scale)
}

fn GenericDouble[T:! `<2>MulWith(f64)`](x: T) -> `<3>auto` {
  let scale: f64 = 2.0;
  return x * scale;
  // => p.(`<1>MulWith(f64)`.Op)(scale)
}

Same pattern provides generic implicit conversions, common types, etc.

Systematically generic language foundations ensure that generic code is just code

Conclusion

Generic programming is better with checking

Entire language is better with foundations built on checked generics

Carbon is developing and exploring this area

Next up, a break and then part 2:

The how of checked generics

Thank you!