1

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?

user2736012
  • 3,543
  • 16
  • 13

2 Answers2

3

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!)

Alec Teal
  • 5,770
  • 3
  • 23
  • 50
  • If this answer is messier than I think it is comment and I'll clean it up. – Alec Teal Dec 11 '13 at 18:41
  • I'd like to avoid having the user of the package do another check for a returned error or a panic recovery. That's pretty much why I'd like the compiler to enforce it. – user2736012 Dec 11 '13 at 18:45
  • @user2736012 if it's not a high-performance application don't worry about it. Checks are good, not checking is bad, if this is an expression that can be used at runtime (read not a `constexpr` in C++ terms) there is no practical way to do it, let alone in Go. I seriously recommend you "fail hard" - if something is wrong you panic and kick and scream IMMEDIATELY - don't do a PHP and chug on. – Alec Teal Dec 11 '13 at 18:52
  • Yes, but wouldn't it be preferable to make it so that it can't be wrong? I'm also just learning C++ right now, and I believe the equivalent I'm looking for would be like an `enum`. I didn't quite know how to express it in my question. – user2736012 Dec 11 '13 at 18:55
  • @user2736012 it depends on what you are doing, suppose it's a double between 2.0 and 5.0, enums wouldn't work. Suppose you have a range of 20, it'd get boring and old. From the sounds of it you are new and I really urge you to go mad with checks, experience will guide you to a nice style (and you'll come in from above, having too few checks would increase the time of your learning curve) – Alec Teal Dec 11 '13 at 18:57
  • Yeah, I didn't mean anything like a wide range of numbers. I mean more like a small but important and meaningful set of options. – user2736012 Dec 11 '13 at 18:59
  • @user2736012 that sounds like an enum to me. – Alec Teal Dec 11 '13 at 19:00
  • So if I understand, Go has nothing like that, and my approach above would be the most concise way? – user2736012 Dec 11 '13 at 19:03
  • [This Stack Overflow answer](http://stackoverflow.com/questions/14426366/what-is-an-idiomatic-way-of-representing-enums-in-golang) describes how to define an enum. (You could still deliberately construct a value outside the defined range, but the Go stdlib nonetheless uses enums for, e.g., system calls where a restricted set of flags is acceptable.) – twotwotwo Dec 11 '13 at 20:29
  • @user2736012 Don't try to program C++ in Go. The Go approach is to check your input and document what you expect. Static assertions aren't the Go approach. – fuz Dec 11 '13 at 20:42
  • @FUZxxl exactly, Go should be thought of as a less crappy Java with great parallel programming features. – Alec Teal Dec 11 '13 at 21:32
  • @Alec While I do not agree with the first part of your statement (The flavour of OO in Go is quite different from Java and IMHO not really in any way comparable), I do agree with the second. – fuz Dec 11 '13 at 21:41
  • @FUZxxl: I'm not trying to program C++ in Go. I was hoping that there was a concise way to create a type consisting of a small number of specific values that could be checked by the compiler. Doesn't seem like an unreasonable possibility for a statically typed language. – user2736012 Dec 11 '13 at 23:54
  • @user2736012 That's what I mean by programming C++ in Go. The idea that as much as possible should be checked (pseudo) statically is not deeply rooted in the Go language. – fuz Dec 12 '13 at 06:44
  • @FUZxxl: Really? Seems to me that just the opposite is true. I know very little about C++ at this point, but I do know that there are implicit conversions that take place, which are not allowed in Go. Like using numbers as boolean conditions, or automatic promotion of numeric values. Looks to me like Go does more static checking than C++. – user2736012 Dec 12 '13 at 14:47
  • @user2736012 Go does more *typechecking* but cannot do *value checking*. – fuz Dec 12 '13 at 19:22
1

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
}
ChrisH
  • 4,788
  • 26
  • 35
  • Thanks for the input. That's an interesting solution. It won't quite work for what I need right now because there are actually several of these groups of values, and it would make the API a little odd. Also, sometimes we need to receive values from more than one group at the same time. But I could see how this would be useful in some cases. – user2736012 Dec 12 '13 at 02:33