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 interface
s 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 contract
s.
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:
- Programming educators have been using “contract” to explain interfaces for decades,
- A
contract
type would obviously be easy to reason about, - This is a use-case for interfaces that Go does not yet accommodate,
- I do not think this would make Go programs more fragile or complex, and
- (I think) it would not have any backward compatibility issues.
I contend this would be a great addition to Go.
What do you think?