How to statically limit function arguments to a subset of values

350 views Asked by At

How does one statically constrain a function argument to a subset of values for the required type?

The set of values would be a small set defined in a package. It would be nice to have it be a compile-time check instead of runtime.

The only way that I've been able to figure out is like this:

package foo

// subset of values
const A = foo_val(0)
const B = foo_val(1)
const C = foo_val(2)

// local interface used for constraint
type foo_iface interface {
    get_foo() foo_val
}

// type that implements the foo_iface interface
type foo_val int
func (self foo_val) get_foo() foo_val {
    return self
}

// function that requires A, B or C
func Bar(val foo_iface) {
    // do something with `val` knowing it must be A, B or C
}

So now the user of a package is unable to substitute any other value in place of A, B or C.

package main

import "foo"

func main() {
    foo.Bar(foo.A) // OK
    foo.Bar(4)     // compile-time error
}

But this seems like quite a lot of code to accomplish this seemingly simple task. I have a feeling that I've overcomplicated things and missed some feature in the language.

Does the language have some feature that would accomplish the same thing in a terse syntax?

2

There are 2 answers

1
ChrisH On

A slightly different approach that may suit your needs is to make the function a method of the type and export the set of valid values but not a way to construct new values.

For example:

package foo

import (
    "fmt"
)

// subset of values
const A = fooVal(0)
const B = fooVal(1)
const C = fooVal(2)

// type that implements the foo_iface interface
type fooVal int

// function that requires A, B or C
func (val fooVal) Bar() {
    fmt.Println(val)
}

Used by:

package main

import "test/foo"

func main() {
    foo.A.Bar() // OK, prints 0
    foo.B.Bar() // OK, prints 1
    foo.C.Bar() // OK, prints 2
    foo.4.Bar()     // syntax error: unexpected literal .4
    E := foo.fooVal(5) // cannot refer to unexported name foo.fooVal
}
16
Alec Teal On

Go can't do this (I don't think, I don't think a few months makes me experienced)

ADA can, and C++ can sometimes-but-not-cleanly (constexpr and static_assert).

BUT the real question/point is here, why does it matter? I play with Go with GCC as the compiler and GCC is REALLY smart, especially with LTO, constant propigation is one of the easiest optimisations to apply and it wont bother with the check (you are (what we'd call in C anyway) statically initialising A B and C, GCC optimises this (if it has a definition of the functions, with LTO it does))

Now that's a bit off topic so I'll stop with that mashed up blob but tests for sane-ness of a value are good unless your program is CPU bound don't worry about it.

ALWAYS write what it easier to read, you'll be thankful you did later

So do your runtime checks, if the compiler has enough info to hand it wont bother doing them if it can deduce (prove) they wont throw, with constant values like that it'll spot it eaisly.

Addendum

It's difficult to do compile time checks, constexpr in c++ for example is very limiting (everything it touches must also be constexpr and such) - it doesn't play nicely with normal code.

Suppose a value comes from user input? That check has to be at runtime, it'd be silly (and violate DRY) if you wrote two sets of constraints (however that'd work), one for compile one for run.

The best we can do is make the compiler REALLY really smart, and GCC is. I'm sure others are good too ('cept MSs one, I've never heard a compliment about it, but the authors are smart because they wrote a c++ parser for a start!)