Creating a Constant Type and Restricting the Type's Values

I have a question about types of constants which are restricted to certain values and how you accomplish that in Go. Say I create a type unary which has two constant values Positive(1) and Negative(-1) and I want to restrict the user of that type (unary) from creating other values of type unary. Do I achieve this by creating a package and making the values Positive and Negative visible and making the type unary restricted to the containing package? See code below for example

package unary

type unary int////not visible outside of the package unary

const (
    Positive unary = 1//visible outside of the package unary
    Negative unary = -1//visible outside of the package unary
)

func (u unary) String() string {//visible outside of the package unary
    if u == Positive {
        return "+"
    }
    return "-"
}

func (u unary) CalExpr() int {//visible outside of the package unary
    if u == Positive {
        return 1
    }
    return -1
}

Is this the correct way to restrict a type to certain constant values?


Solution 1:

Flaws

Your proposed solution is not safe in a way you want it to be. One can use untyped integer constants to create new values of unary having a different int value than 1 or -1. See this example:

p := unary.Positive
fmt.Printf("%v %d\n", p, p)

p = 3
fmt.Printf("%v %d\n", p, p)

Output will be:

+ 1
- 3

We could change p's value to store the int value 3 which is obviously not equal to Positive nor to Negative. This is possible because Spec: Assignability:

A value x is assignable to a variable of type T ("x is assignable to T") in any of these cases:

  • ...
  • x is an untyped constant representable by a value of type T.

3 is an untyped constant, and it is representable by a value of type unary which has underlying type int.

In Go you can't have "safe" constants of which "outsider" packages cannot create new values of, for the above mentioned reason. Because if you want to declare constants in your package, you can only use expressions that have "untyped" versions–which may be used by other packages too in assignments (just as in our example).

Unexported struct

If you want to fulfill the "safe" part, you may use unexported structs, but then they cannot be used in constant declarations.

Example:

type unary struct {
    val int
}

var (
    Positive = unary{1}
    Negative = unary{-1}
)

func (u unary) String() string {
    if u == Positive {
        return "+"
    }
    return "-"
}

func (u unary) CalExpr() int {
    return u.val
}

Attempting to change its value:

p := unary.Positive

p.val = 3 // Error: p.val undefined (cannot refer to unexported field or method val)

p = unary.unary{3} // Error: cannot refer to unexported name unary.unary
// Also error: implicit assignment of unexported field 'val' in unary.unary literal

Note that since we're now using a struct, we can further simplify our code by adding the string representation of our values to the struct:

type unary struct {
    val int
    str string
}

var (
    Positive = unary{1, "+"}
    Negative = unary{-1, "-"}
)

func (u unary) String() string { return u.str }

func (u unary) CalExpr() int { return u.val }

Note that this solution still has a "flaw": it uses exported global variables (more precisely package-level variables) whose values can be changed by other packages. It's true that other packages cannot create and assign new values, but they can do so with existing values, e.g.:

unary.Positive = unary.Negative

If you want to protect yourself from such misuse, you also have to make such global variables unexported. And then of course you have to create exported functions to expose those values, for example:

var (
    positive = unary{1}
    negative = unary{-1}
)

func Positive() unary { return positive }

func Negative() unary { return negative }

Then acquiring/using the values:

p := unary.Positive()

Interface

Care must be taken if you plan to use an interface type for your "constants". An example can be seen in Kaveh Shahbazian's answer. An unexported method is used to prevent others from implementing the interface, giving you the illusion that others truly can't implement it:

type Unary interface {
    fmt.Stringer
    CalExpr() int
    disabler() // implementing this interface outside this package is disabled
}

var (
    Positive Unary = unary(1)  // visible outside of the package unary
    Negative Unary = unary(-1) // visible outside of the package unary
)

type unary int // not visible outside of the package unary

func (u unary) disabler() {}

func (u unary) String() string { /* ... */ }

func (u unary) CalExpr() int { /* ... */ }

This is not the case however. With a dirty trick, this can be circumvented. The exported Unary type can be embedded, and an existing value can be used in order to implement the interface (along with the unexported method), and we can add our own implementations of the exported methods, doing / returning whatever we want to.

Here is how it may look like:

type MyUn struct {
    unary.Unary
}

func (m MyUn) String() string { return "/" }

func (m MyUn) CalExpr() int { return 3 }

Testing it:

p := unary.Positive
fmt.Printf("%v %d\n", p, p)

p = MyUn{p}
fmt.Printf("%v %d\n", p, p.CalExpr())

Output:

+ 1
/ 3

Special case

As Volker mentioned in his comment, in your special case you could just use

type unary bool

const (
    Positive unary = true
    Negative unary = false
)

As the type bool has two possible values: true and false, and we've used all. So there are no other values that could be "exploited" to create other values of our constant type.

But know that this can only be used if the number of constants is equal to the number of possible values of the type, so the usability of this technique is very limited.

Also keep in mind that this does not prevent such misuses when a type of unary is expected, and someone accidentally passes an untyped constant like true or false.

Solution 2:

If you like to just work with int without introducing a wrapper type: a classic way to do that in Go is using a public interface with a private function; so everybody can use it but nobody can implement it; like:

type Unary interface {
    fmt.Stringer
    CalExpr() int
    disabler() //implementing this interface outside this package is disabled
}

var (
    Positive Unary = unary(1)  //visible outside of the package unary
    Negative Unary = unary(-1) //visible outside of the package unary
)

type unary int //not visible outside of the package unary

func (u unary) disabler() {}

func (u unary) String() string { //visible outside of the package unary
    if u == Positive {
        return "+"
    }
    return "-"
}

func (u unary) CalExpr() int { //visible outside of the package unary
    if u == Positive {
        return 1
    }
    return -1
}

Others are able to set Positive to nil though; but that's not a thing in Go world - in such cases.

As @icza mentioned, one can overwrite public methods. But for private methods, Go will not call "the most shallow" one, but instead calls the original.