Type check the empty interface{}
This is an experiment.
// #type T1, T2
Without type checking:
/* */ package main
/* */
/* */ type Numeric interface{}
/* */
/* */ func main() {
/* */ var n Numeric
/* OK */ n = 3
/* OK */ n = 3.14
/* OK */ n = "abcd"
/* */ _ = n
/* */ }
With type checking:
/* */ package main
/* */
/* */ type Numeric interface{
/* --> */ // #type int, float64
/* */ }
/* */
/* */ func main() {
/* */ var n Numeric
/* OK */ n = 3
/* OK */ n = 3.14
/* ERR */ n = "abcd"
/* */ _ = n
/* */ }
Execute the checker to get the error:
$ interface-type-check .
testfile.go:10:6: cannot use "bad value" (constant of type string) as Numeric value in variable declaration: mismatching sum type (have string, want a type in interface{type int, float64})
Prebuilt binaries are available here as well as in the release page.
git clone https://github.com/siadat/interface-type-check
cd interface-type-check
make test build
Given the declaration:
type Numeric interface{
// #type int, float64
}
The following checks are performed:
var number Numeric = "abc" // CHECK ERR: expected int or float
_, _ = number.(string) // CHECK ERR: string not allowed
switch number.(type) {
case string: // CHECK ERR: string not allowed
case float:
}
switch number.(type) { // CHECK ERR: missing case for int
case float:
}
switch number.(type) { // CHECK ERR: missing case for nil
case float:
case int:
}
More examples: fork/src/types/examples/sum.go2
All supported types of encoding/json.Token are known, as documented here:
// A Token holds a value of one of these types:
//
// Delim, for the four JSON delimiters [ ] { }
// bool, for JSON booleans
// float64, for JSON numbers
// Number, for JSON numbers
// string, for JSON string literals
// nil, for JSON null
//
type Token interface{}
Adding the #type comment, it would look like this:
type Token interface {
// #type Delim, bool, float64, Number, string
}
That's all we need to be able to use the checker.
database/sql.Scanner is also defined as an empty interface whose possible types are known.
Before:
// Scanner is an interface used by Scan.
type Scanner interface {
// Scan assigns a value from a database driver.
//
// The src value will be of one of the following types:
//
// int64
// float64
// bool
// []byte
// string
// time.Time
// nil - for NULL values
//
Scan(src interface{}) error
}
After:
// Scanner is an interface used by Scan.
type Scanner interface {
Scan(src SourceType) error
}
type SourceType interface {
// #type int64, float64, bool, []byte, string, time.Time
}
The standard library defines one net.IP type for both IPv4 and IPv6 IPs:
// An IP is a single IP address, a slice of bytes.
// Functions in this package accept either 4-byte (IPv4)
// or 16-byte (IPv6) slices as input.
type IP []byte
This type has a String() function, which relies on runtime checks to detect the version of the IP here:
if p4 := p.To4(); len(p4) == IPv4len { ...
There are very good reasons to use a simple []byte data structure for the IPs.
I am not suggesting that this code should change.
I am only running tiny hypothetical experiments. With that in mind, let's write it using // #type
:
type IPv4 [4]byte
type IPv6 [16]byte
type IP interface {
// #type IPv4, IPv6
}
func version(ip IP) int {
switch ip.(type) {
case IPv4: return 4
case IPv6: return 6
case nil: panic("ip is nil")
}
}
The Connecting type has a retry field:
type Connected struct{}
type Disconnected struct{}
type Connecting struct{ rety int }
type Connection interface {
// #type Connected, Disconnected, Connecting
}
func log(conn Connection) int {
switch c := conn.(type) {
case Connected: fmt.Println("Connected")
case Disconnected: fmt.Println("Disconnected")
case Connecting: fmt.Println("Connecting, retry:", c.retry)
case nil: panic("conn is nil")
}
}
Empty interfaces are used when we want to store variables of different types which don't implement a common interface.
There are two general use cases of an empty interface:
You should not use this checker for 1. Sometimes we do not have prior knowledge about the expected types. For example, json.Marshal(v interface{}) is designed to accept structs of any type. This function uses reflect to gather information it needs about the type of v. In this case, it is not possible to list all the supported types.
You could consider using it, when all the types you support are known at the type of writing your code.
This is particularly useful when the types are primitives (eg int), where we have to create a new wrapper type (eg type Int int) and implement a non-empty interface on it.
This tool is designed to work with code written in the current versions of Go (ie Go1). The current design draft of Go2 includes the type list:
type Numeric interface {
type int, float64
}
At the moment, the type list is intended for function type parameters only.
interface type for variable cannot contain type constraints
The draft notes:
Interface types with type lists may only be used as constraints on type parameters. They may not be used as ordinary interface types. The same is true of the predeclared interface type comparable.
This restriction may be lifted in future language versions. An interface type with a type list may be useful as a form of sum type, albeit one that can have the value nil. Some alternative syntax would likely be required to match on identical types rather than on underlying types; perhaps type ==. For now, this is not permitted.
The highlight section is what this experiment addresses via an external type checking tool.
You might think of this tool as an experiment to see whether a sum type would be a valuable addition to the language.
// #type T1, T2
Do any of these: