-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
enableif: simpler and more powerful alternative to concepts
#12048
Conversation
Why not just use |
Because it doesn't work. |
You should probably create an RFC before implementing something like this. Personally I am still in favour of changing concepts to use a more standard interface-like syntax (and extend them to work at runtime): type
Addition = concept
proc `+`(x, y: Addition): Addition I don't think these sort of ad-hoc solutions is what we need to make the language coherent. |
I have an RFC in the works that is mostly like "generics need to be type-checked completely" (note: generics, not only their instantiations) and a mutation of concepts would allow us to do that, |
1174b7e
to
f87f88e
Compare
please write a separate RFC for your suggestion so we can follow-up there and compare apples to apples against this PR; IIUC, this doesn't cover the use cases of proc fun(x: auto){.enableif: compiles(x+x).} = echo x+x # or `overloadExists(x+x)` pending https://github.com/nim-lang/RFCs/issues/164
fun 2.3 # ok
type A = object
template `+`(a, b: A): auto = A()
fun A() # ok
fun 'a' # correctly gives Error: enableIf condition failed: 'compiles(x + x)' whereas your suggestion would be more verbose (requires a separate type), and fail for
there's nothing ad-hoc about Furthermore, it's proven to work:
as mentioned, that wouldn't work, however parser could be changed to support this instead of a pragma: type T1 = int # irrelevant for `where` clause
where T1.sizeof == T2.sizeof: # T1 binds to generic inside `fun`, not to line above
proc fun[T1, T2](a1: T1, a2: T2): auto = discard
so I went for |
Hm, but how does this replace the planned (albeit long delayed) feature of having concepts act like golang's interfaces (runtime polymorphism)? |
you can build it as a library solution on top of type geometry interface {
area() float64
perim() float64
}
func measure(g geometry) {
fmt.Println(g)
fmt.Println(g.area())
fmt.Println(g.perim())
} template geometry(a): untyped = (type(a.area) is float and type(a.perim) is float)
proc measure(g: auto) {.enableif: geometry(g).} =
echo type(g) # tuple[area: float64, perim: float64]
echo g
echo g.area
echo g.perim
measure((area: 1.1, perim: 1.2)) the point is, it's more flexible as it can consider whole signature, and it's much simpler as it just reuses existing expression evaluator. here's with concepts: type geometry = concept a
a.area is float
a.perim is float
proc measure(g: geometry) =
echo type(g) # prints: `geometry` instead of `tuple[area: float64, perim: float64]`
echo g
echo g.area
echo g.perim
type T = tuple[area: float, perim: float]
var t: T
measure(t) and this doesn't even work with concepts (yet another edge case: hits #9573): measure((area: 1.1, perim: 1.2)) # Error: no tuple type for constructor |
That example doesn't appear to tackle runtime polymorphism, only compile time polymorphism. What I meant was, there were plans to have concepts be used in a way similar to golang's Interfaces, so you could have (to take your example) a sequence of geometry objects that have varying implementations |
That's surprising. Interfaces can already be implemented using macros, and if you really wanted to integrate them with concepts, you could because macros can inspect contepts. |
createInterface(Geom):
proc area(this: Geom): float
proc perim(this: Geom): float
proc measure(a: seq[Geom]) = discard
measure(@[toGeom GeomCustom(area: 0.0, perim: 0.1), toGeom (area: 1.0, perim: 2.0)])
# or even use a `converter` to avoid the `toGeom` in some places, although I don't like converters too much |
b55cf33
to
661392c
Compare
added tests, ready for review (test failures unrelated) |
I don't have a strong preference about |
Concepts can be kept for backward compatibility but existing code relying on concepts can be migrated to enableif without loss of functionality, as
I'm not aware of a concrete proposal/RFC/PR to extend concepts to runtime interfaces (which, as mentioned, can already be done with library solutions anyways). AFAIK concepts are plagued with fundamental issues that are hard/impossible to fix (eg type/value implicit conversion as mentioned in nim-lang/RFCs#13 or the many issues mentioned in
Concepts
that are so bad they prevent using concepts in compiler sources) . |
@timotheecour I see a few issues with
|
Once again, I think this approach is fundamentally wrong. I strongly advocate for the solution described in my post above. Can we close this and stop spending time on discussing it further? |
Instead of closing this PR, if you strongly advocate for your solution you could open a RFC (or PR) to explain your alternative proposal in sufficient detail so there is something concrete to compare against (and maybe include in the description how it addresses the points in #12048 (comment) or how it'd fix the concept issues mentioned in top post that |
@dom96 I don't see how you would express something like: type
Iterable*[T] = concept c
for x in items(c): x is T
for x in mitems(c): x is var T
Indexable*[T] = concept c
type IndexType = type(c.low)
var i: IndexType
c[i] is T
c[i] is var T
var value: T
c[i] = value
c.len is IndexType In my opinion, concepts are so powerful mainly because they don't enforce a traditional interface syntax. As long as the path forward isn't clear, we shouldn't cut the discussion short. The key question might be if it is actually possible to solve the exponential complexity issue of concepts. |
I don't think that this PR fixes any of the mentioned problems of concepts, because this is orthogonal to concepts. It is simply a different feature. When I then ignore the concepts issues that this PR claims to fix, there is no issue left that is actually fixed. It is just jet another complication of the Nim language. If you want this feature to be part of the language, we need a real use case for it. No, there should be multiple use cases. Something that does contain neith And just because language X does have this complication, doesn't mean that we want to introduce this complication in Nim as well. Nim has a very different feature set than any other language out there. |
I'm assuming your comment was referring to "@dom96 's proposal", not proc Iterable(U: typedesc): bool =
var c: U
type T = type(items(c)) # or type T = type(for x in items(c): x)
type(mitems(c)) is var T # or: (type(for x in mitems(c): x) is var T)
proc Indexable(U: typedesc): bool =
var c: U
type IndexType = type(c.low)
var i: IndexType
type T = type(c[i])
doAssert c[i] is T
doAssert c[i] is var T
var value: T
doAssert compiles((c[i] = value))
type(c.len) is IndexType
proc fun[U](c: U){.enableif: Iterable(U).} =
for x in items(c):
echo x
fun @[1,2,3]
proc fun2[U](c: U){.enableif: Indexable(U).} =
echo c[0]
fun2 @[1,2] note that your'e using const z = 1
doAssert z is var int # passes
this is actually better with enableif: for one-off constraints, just put the constraint inline, which is less boilerplate than introducing a concept just for that. For re-used constraints, just use an auxiliary proc/template. Nothing complicated/unusual here.
pragma is part of the proc signature anyway (eg gcsafe etc), so IMO that's a subjective point; more importantly that's what enables constraints on whole signature
this is a valid point. It can be done using a library solution via a macro
not sure I follow. Both enableif and concepts enforce type constraints, which allows in particular overloading by static properties (duck typing) or early failing (via proc signature), eg
|
If this works, you effectively reimplemented a variation of concepts with a different syntax: proc Indexable(U: typedesc): bool =
var c: U
type IndexType = type(c.low)
var i: IndexType
type T = type(c[i])
doAssert c[i] is T
doAssert c[i] is var T
var value: T
doAssert compiles((c[i] = value))
type(c.len) is IndexType
proc fun[U](c: U){.enableif: Iterable(U).} =
for x in items(c):
echo x
fun @[1,2,3] Can we use the |
661392c
to
ca4ff35
Compare
Unfortunately, this "simpler" alternative to concepts misses some of the key capabilities:
No, because of the limitations mentioned in the first paragraph.
Concepts are plagued by a lack of development resources - people who have enough time and willingness to dive in the existing code and improve it. There are no fundamental issues that are impossible to fix.
It was always envisioned that all concepts will be backed by an instantiation cache just like generic procs and types. This is not particularly hard to do, but it hasn't been implemented yet and it should be the source of compilation slowdowns that people report. |
This feels like Python Decorator-ish. 🤔 Like an anonymous JavaScript function on each proc will get long and would be hard to test/debug. |
@bluenote10 easy, here is how: type
Iterable*[T] = concept
iterator items(x: Iterable): T
iterator mitems(x: Iterable): var T
Indexable*[T, IndexType] = concept
proc low(x: Indexable): IndexType
proc `[]`(x: Indexable, i: IndexType): T
proc `[]`(x: Indexable, i: IndexType): var T
proc `[]=`(x: Indexable, i: IndexType, val: T)
proc len(x: Indexable): IndexType Am I missing something? This syntax is also far easier to understand as it doesn't require you to evaluate Nim code in your head to understand the concept spec. There are undoubtedly crazy things that concepts let you do, but I would argue that they are niche cases which don't deserve to be in a spec like this (as they will be far too complex to understand at a glance). With my syntax it's very trivial to see which procs/iterators/whatever are missing and the compiler will be able to give a very clear error message as well. |
Also the point of the example was to show that with the current syntax it's optional to pull out a type into the generic. It can be inferred as well if it is not essential. I personally prefer to read the concept code to see what I have to provide eventually, but I'm free to choose how exactly I implement it. |
Very good point. But I think this is something that can be solved by either softening what
Personally I think this is what makes the concepts syntax so bad. You'll be figuring out what each type evaluates to every time you try to understand a new concept. It's a good thing that this is disallowed. I'm writing an RFC, will edit this post to link to the PR once I submit it. Let's discuss this further in there. Edit: RFC -> nim-lang/RFCs#167 |
I was thinking back of the PR that attempted to introduce |
@bluenote10 for |
The same arguments are repeated in every discussion about the syntax of concepts. The proc declaration approach is a bit misleading, because you can implement a "proc" requirement with a template or a method. As a minimum, a concept should also be able to check for the presence of associated types and constants, so you'll need to add additional specialized syntax for this as well. How would that look? Perhaps, something like this: type C = concept
const C.signatureSize: int
type C.ElemType Also, please write a concept requiring a type for which all of the fields are serializable. Or another one for a type that can be initialized from a value that's convertible to string? These seem like a reasonable requirements that may arise in the context of a generic library. |
fdcd4f4
to
be51e27
Compare
And to complete the mess, here is my RFC nim-lang/RFCs#168 |
be51e27
to
3b6dcd8
Compare
Sorry, rejected. Concepts are our way to type-check generics and |
this PR implements
enableif
for routine (generic proc/template/etc) specialization, known in other languages as:enable_if
can be hard to use in C++)eg usage:
Benefits compared to concepts / alternatives:
enableif
expression returns true right after sigmatchsince
doesn't work with templates timotheecour/Nim#42) (I need to double check this point) :concepts
can only model single argument constraints; EDIT: eg where this matters: see std/lists: Various changes tolists
(RFC #303) #16536 (comment){.enableif: isFoo(T).}
){.explain.}
) is needed to debug a enableif expression: you can just insertecho
statements inside the enableif expression, it'll execute as regular compile time code during sigmatch(and it gets worse when
Foo[T]
is needed, givingFoo[inferred[T], Foo]
)wheres
enableif: compiles(a.x)
will just showB
concept
issues avoided byenableif
There are many issues with concepts, see Concepts
These issues have prevented using concepts in the compiler source code. Eg see comments like this #9733 (comment):
I believe using
enableif
will solve most of those issues, while also enable use cases that aren't possible with concepts.I tried porting a few examples from concept to enableif and the problem disappeared with enableif in every case.
Would be interesting to try other issues and see which are fixed and which are not. /cc @dawkot
would close [concept] generic parameter access via Foo.T doesn't work with concepts #9006 (generic parameter access via Foo.T doesn't work with concepts )
this works:
proc fun[U](s: U) {.enableif: U is Foo[U.T] .} = type T2 = s.type.T
would close
type(foo)
interpreted asfoo
inside concept #10079 (type(foo)
interpreted asfoo
inside concept)the straightforward porting to enableif works. There is no
type(x) vs x
conflation with enableif that causes ambiguitieswould close Converter + concept results in an endless (or very long) compiler loop. #8197 (Converter + concept results in an endless (or very long) compiler loop)
the straightforward porting works fine:
converter toBool*(arg: auto): bool {.enableif: type(len(arg)) is int.} = arg.len > 0
would close concept cannot be overloaded with var concept #9733 (concept cannot be overloaded with var concept)
would close Concepts with inheritance causing 'SIGSEGV: Illegal storage access ...' #10506 (Concepts with inheritance)
proc conceptf(f : auto): string {.enableif: type(f(f)) is string .} = f(f)
workswould close
type
proc used on a concept-constrained variable return void #9732 (type
proc used on a concept-constrained variable return void)func foo(a: auto): type(a) {.enableif: true.} = discard
workswould close Error when a concept-bound generic function argument is a tuple #9573 Error when a concept-bound generic function argument is a tuple
notes
I say
would close
for those issues because, while merging this PR wouldn't fix the problem with concepts, i would provide a suitable alternative via enableif, and there's no point in keeping unfixable issues open forever if suitable workaround is made available.bikeshedding: I could rename
enableif
towhere
:enableif
is used in C++ butwhere
is what's used inC#
,swift
,rust
,haskell
for the closest semantic thing; and a bit easier to type (and no guesswork enableif vs enableIf); on the other handenableif
is more unique/easier to searchadd testsdone[EDIT]: see also Having problems with concepts that won't finish compiling - Nim forum : Having problems with concepts that won't finish compiling - Nim forum