英文:
How to statically limit function arguments to a subset of values
问题
如何将函数参数静态约束为所需类型的值子集?
这个值集将是在一个包中定义的一个小集合。最好是在编译时进行检查,而不是在运行时。
我能想到的唯一方法是这样的:
package foo
// 值子集
const A = foo_val(0)
const B = foo_val(1)
const C = foo_val(2)
// 用于约束的本地接口
type foo_iface interface {
get_foo() foo_val
}
// 实现 foo_iface 接口的类型
type foo_val int
func (self foo_val) get_foo() foo_val {
return self
}
// 需要 A、B 或 C 的函数
func Bar(val foo_iface) {
// 使用 `val` 做一些操作,知道它必须是 A、B 或 C
}
这样,包的用户就无法将任何其他值替换为 A
、B
或 C
。
package main
import "foo"
func main() {
foo.Bar(foo.A) // 正常
foo.Bar(4) // 编译时错误
}
但是这似乎是一种相当冗长的代码来完成这个看似简单的任务。我觉得我可能过于复杂化了,可能错过了语言中的某些特性。
这种语言是否有一些特性可以用简洁的语法实现相同的功能呢?
英文:
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?
答案1
得分: 3
Go不能做到这一点(我认为,几个月的经验并不能让我有经验)。
ADA可以,C++有时可以,但不够干净(constexpr和static_assert)。
但是,真正的问题/观点在于,这有什么关系呢?我使用GCC作为Go的编译器,GCC非常聪明,特别是在使用LTO时,常量传播是最容易应用的优化之一,它不会费心进行检查(在C中,我们称之为静态初始化A、B和C,GCC会对此进行优化(如果它有函数的定义,使用LTO时会有))。
现在这有点离题了,所以我会停止这个混乱的讨论,但是对于值的合理性进行测试是好的,除非你的程序受限于CPU,不要担心它。
始终编写易于阅读的代码,你以后会感激自己的
所以进行运行时检查,如果编译器有足够的信息,它就不会费心进行检查,如果它能够推断(证明)它们不会抛出异常,对于像那样的常量值,它会轻松发现。
附加说明
进行编译时检查很困难,例如在C++中的constexpr非常受限制(它触及的一切也必须是constexpr等),它与普通代码不兼容。
假设一个值来自用户输入?那个检查必须在运行时进行,如果你写了两套约束条件(不管怎么工作),一套用于编译,一套用于运行,那将是愚蠢的(并且违反DRY原则)。
我们能做的最好的办法就是让编译器变得非常聪明,而GCC就是如此。我相信其他编译器也很好(除了微软的编译器,我从来没有听说过对它的赞美,但是编写C++解析器的作者肯定很聪明!)
英文:
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!)
答案2
得分: 1
一个稍微不同的方法可能适合你的需求,那就是将函数作为类型的方法,并导出一组有效的值,但不能构造新的值。
例如:
package foo
import (
"fmt"
)
// 值的子集
const A = fooVal(0)
const B = fooVal(1)
const C = fooVal(2)
// 实现 foo_iface 接口的类型
type fooVal int
// 需要 A、B 或 C 的函数
func (val fooVal) Bar() {
fmt.Println(val)
}
使用方法:
package main
import "test/foo"
func main() {
foo.A.Bar() // 正常,输出 0
foo.B.Bar() // 正常,输出 1
foo.C.Bar() // 正常,输出 2
foo.4.Bar() // 语法错误:意外的字面量 .4
E := foo.fooVal(5) // 无法引用未导出的名称 foo.fooVal
}
英文:
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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论