6. Parametric Constraints

Your interface definitions, which can later be used as constraints, can accept type parametersarrow-up-right as well.

// The store interface represents a store that sells products.
// It takes a type parameter P that represents the type of products the store sells.
type store[P product] interface {
	Sell(P)
}

type product interface {
	Price() float64
	Name() string
}

type book struct {
	title  string
	author string
	price  float64
}

func (b book) Price() float64 {
	return b.price
}

func (b book) Name() string {
	return fmt.Sprintf("%s by %s", b.title, b.author)
}

type toy struct {
	name  string
	price float64
}

func (t toy) Price() float64 {
	return t.price
}

func (t toy) Name() string {
	return t.name
}

// The bookStore struct represents a store that sells books.
type bookStore struct {
	booksSold []book
}

// Sell adds a book to the bookStore's inventory.
func (bs *bookStore) Sell(b book) {
	bs.booksSold = append(bs.booksSold, b)
}

// The toyStore struct represents a store that sells toys.
type toyStore struct {
	toysSold []toy
}

// Sell adds a toy to the toyStore's inventory.
func (ts *toyStore) Sell(t toy) {
	ts.toysSold = append(ts.toysSold, t)
}

// sellProducts takes a store and a slice of products and sells
// each product one by one.
func sellProducts[P product](s store[P], products []P) {
	for _, p := range products {
		s.Sell(p)
	}
}

func main() {
	bs := bookStore{
		booksSold: []book{},
	}

    // By passing in "book" as a type parameter, we can use the sellProducts function to sell books in a bookStore
	sellProducts[book](&bs, []book{
		{
			title:  "The Hobbit",
			author: "J.R.R. Tolkien",
			price:  10.0,
		},
		{
			title:  "The Lord of the Rings",
			author: "J.R.R. Tolkien",
			price:  20.0,
		},
	})
	fmt.Println(bs.booksSold)

    // We can then do the same for toys
	ts := toyStore{
		toysSold: []toy{},
	}
	sellProducts[toy](&ts, []toy{
		{
			name:  "LEGO bricks",
			price: 10.0,
		},
		{
			name:  "Barbie",
			price: 20.0,
		},
	})
	fmt.Println(ts.toysSold)
}

What's Happening Here?

You're creating a generic interface - an interface that has a type parameter.

Look at the Existing Code

The architect already wrote these types:

userBiller

orgBiller

The Pattern

Notice:

  • userBiller has Charge(u user) - takes a user

  • orgBiller has Charge(o org) - takes an org

  • Both have Name() string

The Solution

You need to create an interface that can represent both of these:

Breaking Down the Solution

What [C customer] Means

  • C is a type parameter

  • C must be a customer (either user or org)

The Methods

  • Takes a parameter of type C (whatever customer type)

  • Returns a bill

  • Returns a string

How It Works

When C = user:

When C = org:

Complete Solution

That's it! Just those 3 lines.

Why This Works

userBiller implements biller[user] because:

  • βœ“ Has Charge(user) bill method

  • βœ“ Has Name() string method

orgBiller implements biller[org] because:

  • βœ“ Has Charge(org) bill method

  • βœ“ Has Name() string method

Comparison to Store Example

In the lesson example:

In your assignment:

Same pattern! The interface has a type parameter and uses it in a method signature.

Visual Understanding

Assignment

The chief architect at Textio has decided she wants to implement billing with generics. Specifically, she wants us to create a new biller interface. A biller is an interface that can be used to charge a customer, and it can also report its name.

There are two kinds of billers:

  • userBiller (cheaper)

  • orgBiller (more expensive)

A customer is either a user or an org. A user will be billed with a userBiller and an org with an orgBiller.

Create the new biller interface. It should have 2 methods:

  • Charge

  • Name

The good news is that the architect already wrote the userBiller and orgBiller types for us that fulfill this new biller interface. Use the definitions of those types and their methods to figure out how to write the biller interface definition.

Solution

1

Start Simple: What Are You Creating?

You're creating an interface (a contract that says "types must have these methods").

2

Step 1: Look at What Already Exists

The code already has userBiller with these methods:

And orgBiller with these methods:

3

Step 2: What Do They Have in Common?

Both have:

  • A Charge method

  • A Name method

But Charge is different:

  • userBiller.Charge takes a user

  • orgBiller.Charge takes an org

4

Step 3: How Do We Make ONE Interface for Both?

Use a type parameter so the interface can work with different customer types: