Proposal: Contracts in Go (Not Generics)

Photo by Cytonn Photography on Unsplash

#Update

When I wrote this I did not remember that the draft design for Go2 generics chose to use the name “Contracts” for a new language feature to provide a Go-idiomatic response to generics.

Still, I stand by my use of the name contracts for this proposal as I think it is more in line with the use than the programming industry and used for decades, and that would the Go team named contracts would be better if they renamed it “Constraints”.

#Discussion

Discussion for this proposal is (mostly?) on Github.

Preface

I wanted to write this as a Go Experience Report (like this one), but my I felt that it would have simply taken simply too much time to explain my specific used case, and by then I would have lost the reader. So I decided instead to simple create a proposal instead.

Suffice it to say that it was my experiencing difficulties during development that caused me to write this, not a pie-in-the-sky desire for some shiny new feature. So I hope those in the Go community who prefer experience reports can forgive me for this transgression.

Problem Statement

In my experience, there are generally two broad use-cases for interfaces. The second is what Go is exceptional for; the one-method, implicitly-implemented “-er” type interface. I cannot gush enough how brilliant these are and how wonderfully they encourage serendipitous reusability.

That said, there is another type of broad use-case for interfaces, and it is the one commonly recognized by most other programming languages; the idea of a contract between caller and callee. In my experience I have generally seen the need for these when creating an extension API.

Basically this concept would be useful in Go when you want to explicitly indicate that a package is to be used as an extension for application code and this concept is usually described as a contract.”

An example of the type of extension might be a database driver — what they call a “dialect” — for GORM.

Proposed Addition

“Contracts” would be a new type in Go and would allow a developer to define a set of interfaces to comprise a contract plus then be able to reference the contract in a struct to enforce its usage

If contracts of this nature existed, maybe GORM would have used a contract and instead and created a collection of small interfaces rather than one large interface?

Example Declaration

Using the GORM Dialect type for our example, with a contract it they could make it look like this:

type Dialect contract {
	NameGetter
	DBSetter
	VarBinder
	Quoter
	DataTyper
	IndexVerifier
	ForeignKeyVerifier
	IndexRemover
	TableVerifier
	ColumnVerifier
	ColumnModifier
	LimitAndOffsetConstrainer
	DummyTableSelecter
	LastInsertIDReturningSuffixer
	DefaultValueStringer
	KeyNameBuilder
	CurrentDatabaseGetter
}

And here is what each of those interfaces could look like:

type NameGetter interface {
	GetName() string
}
type DBSetter interface {
	SetDB(db SQLCommon)
}
type VarBinder interface {
	BindVar(i int) string
}
type Quoter interface {
	Quote(key string) string
}
type DataTyper interface {
	DataTypeOf(field *StructField) string
}
type IndexVerifier interface {
	HasIndex(tableName string, indexName string) bool
}
type ForeignKeyVerifier interface {
	HasForeignKey(tableName string, foreignKeyName string) bool
}
type IndexRemover interface {
	RemoveIndex(tableName string, indexName string) error
}
type TableVerifier interface {
	HasTable(tableName string) bool
}
type ColumnVerifier interface {
	HasColumn(tableName string, columnName string) bool
}
type ColumnModifier interface {
	ModifyColumn(tableName string, columnName string, typ string) error
}
type LimitAndOffsetConstrainer interface {
	LimitAndOffsetSQL(limit, offset interface{}) string
}
type DummyTableSelecter() interface {
	SelectFromDummyTable() string
}
type LastInsertIDReturningSuffixer() interface {
	LastInsertIDReturningSuffix(tableName, columnName string) string
}
type DefaultValueStringer() interface {
	DefaultValueStr() string
}
type KeyNameBuilder() interface {
	BuildKeyName(kind, tableName string, fields ...string) string
}
type CurrentDatabaseGetter() interface {
	CurrentDatabase() string
}

Isn’t this more like idiomatic Go?

Example Usage

Then to use a contract in a struct, just embed it as you would another struct. The following example embeds our hypothetical contract named gorm.Dialect. Very simple:

type OracleDialect struct {
    gorm.Dialect
    // Properties go here
}
// Methods go here

Additional Enhancements

Beyond the simple addition of contracts there are a few other features I think would really be beneficials for these proposed contracts.

Allow Interfaces to be Optional

One of my biggest complaints with interfaces in other languages is their fragility. They can never accept backward compatible changes once they have been “released into the wild.” The ability to define implementation of an interface as optional would go a long way to making contracts more resilient over time. It would also allow contract designers to specify which methods implementers can ignore if they do not need them.

Revisiting our Dialect contract from above, here is what it might look like with an optional keyword:

type Dialect contract {
	NameGetter
	DBSetter
	VarBinder
	Quoter
	DataTyper
	IndexVerifier
	ForeignKeyVerifier
	IndexRemover
	TableVerifier
	ColumnVerifier
	ColumnModifier
	LimitAndOffsetConstrainer
	DummyTableSelecter
	LastInsertIDReturningSuffixer optional
	DefaultValueStringer optional
	KeyNameBuilder
	CurrentDatabaseGetter
}

Allow Interfaces to be Added

Now I realize that over time developers might add methods to contracts and mark them as optional to support backward compatibility even though they might not be optional for newer use-cases. If this is seen as a significant problem then maybe added [`<ver_tag>`] could be used to specify that a method is in fact optional but only for backward compatibility reasons and that newer implementers should use it.

Contacts would also allow IDEs like GoLand to optionally enforce the need the interface to be implemented, if they chose to do so.

Counter Argument

So what do I anticipate the counter argument to be?

Go does not need a new type

I assume a seasoned Go developer might say:

“We can already do “contracts” just by embedding an interface into another interface. We don’t need a new named — let’s keep Go — and just allow interfaces to be explicitly implemented in a struct to address this use-case.”

And they would be right. Frankly, that was my first thought too.

Rebuttal

But then I remembered one of the best things about Go interfaces is they are implicit, and when small they can empower serendipitous reusability. But Go currently encourages large interfaces for these use-cases, and from an idiomatic perspective, I believe that is bad behavior.

Contracts would discourage large interfaces

If Go adds a new contract type, and the contracts only supports aggregation of interfaces with not direct inclusion of method signatures then many small interfaces would actually be encouraged.

There would also be be a clear and distinct difference in intended use-case for contracts vs. interfaces.

Summary

So, since:

  1. Programming educators have been using “contract” to explain interfaces for decades,
  2. A contract type would obviously be easy to reason about
  3. This is a use-case for interfaces that Go does not yet accommodate,
  4. I do not think this would make Go programs more fragile or complex, and
  5. (I think) it would not have any backward compatibility issues.

I contend this would be a great addition to Go.

What do you think?

Leave a Reply

Your email address will not be published. Required fields are marked *