Giter Site home page Giter Site logo

Seemingly pointless proc about nimyaml HOT 6 CLOSED

Graveflo avatar Graveflo commented on July 17, 2024
Seemingly pointless proc

from nimyaml.

Comments (6)

flyx avatar flyx commented on July 17, 2024

Good question. This was added in 576cf11 which added support for loading parent fields of a derived type.

For what I remember, I added it because the other constructObjectDefault failed when called directly on a RootObj. It was the more readable solution compared to introducing an edge case check in the other proc. This is an edge case but might be encountered via ref RootObj which can sometimes be useful, I suppose.

This doesn't seem to have made it into a test case, which is why removing the proc doesn't change anything.

Regarding your plan, I don't think it's a good idea because from my point of view, p[T](x:T) is obviously the more specialized candidate to call since it can be instantiated with T=Bar while the other proc uses Foo. Precedence of generics over inheritance feels intuitive while your suggestion doesn't.

If I needed to do something like this, I would try

proc p[T: Foo](x: T): bool = false
proc p[T](x:T): bool = true

but I don't think type classes can be used like this, at least they're not documented to do this.

from nimyaml.

Graveflo avatar Graveflo commented on July 17, 2024

Alright, as long as it was only meant to be an edge case for RootObj() exactly that is workable. I don't think that Nim is broken by its current behavior and by extension your rational makes sense. The instantiation of T is more specific, but the point of generics is to make abstractions as is inheritance. If you compare the signatures as abstractions Foo is far more specific than T since T can be anything. Also I don't think that moving a definition into the constraint is a logical way to deal with overload resolution. There isn't a reason for there to be a trade off in overload matching when you want type variable semantics. I've already made a PR to Nim about that too.
An easy way to get on the track I am thinking without getting heady over the "correct" way to think about things is to consider the purpose of very general signatures like p[T: object](x: T). A proc like this is used for some basic behavior that works for all objects and in the context of overload resolution you are meant to make your overloads specialized by taking precedence over this "wide net" (or even more general, meant to be usurped in certain conditions). I don't see the rational of considering it's instantiation as it's specificity because then we are no longer talking about abstractions at that point. In this sense we are skipping over the mechanics purpose as pretending we are not talking about generics. You can just think of two competing generics if that makes it easier. They may both instantiate to the same concrete type yet one may be more specific than the other.
Thanks for answering my original question. I'll go ahead and close the issue, but I am still interested to hear if you have anything else to say about overload resolution.

from nimyaml.

flyx avatar flyx commented on July 17, 2024

The instantiation of T is more specific, but the point of generics is to make abstractions as is inheritance. If you compare the signatures as abstractions Foo is far more specific than T since T can be anything.

Due to late binding of function calls in generics, p[T](x: T) actually can be more specific than p(x: Foo) when called on Bar():

type
  Foo = object of RootObj
  Bar = object of Foo

proc q(x: Foo): bool = false
proc q(x: Bar): bool = true

proc p(x: Foo): bool = q(x)
proc p[T](x: T): bool = q(x)

echo p(Bar())

Since q(x) in the generic proc is only resolved on instantiation, it calls the second, more specific, q. This scenario might look artificial but assume for example that q is $.


Generally, I would say generics and inheritance are two very different use cases.

Generics are a tool to implement an algorithm or data structure that has abstract requirements on some sort of payload. For example, a sorting algorithm needs indexable payload data with known length and comparable, copyable items, or a hashmap needs a hashable key with equality check. The requirements can be minimal and often are (e.g. a generic growable list only needs the payload items to be movable). The part about the requirements is often implicit because it is complex to codify in a language – just look at how long C++ needed to properly specify concepts.

Inheritance, on the other hand, is used for run-time polymorphism, like when you want to have different kinds of objects in the same container (e.g. for UIs with different widgets), or when you want a service to be able to consume different kinds of objects (e.g. most of what Java Enterprise does). Inheritance is also used for specialization, which is somewhat of a different use-case but from what I've seen usually comes up along run-time polymorphism.

The wide net analogy you mentioned describes specialization, so I'd say it describes inheritance but not generics. Generics are not about specialization, unless you're C++, then you use template specializations for type introspection because the language doesn't offer better compile-time capabilities. But Nim has them so it doesn't need this.


I did a quick test and was able to implement what you want to do with current Nim:

type
  Foo = object of RootObj
  Bar = object of Foo
  DerivedFromFoo = concept x
    x is Foo

proc p(x: DerivedFromFoo): bool = false
proc p[T](x: T): bool = true

echo p(Bar())

You need to write some more lines, but I'd say that this use-case is hardly common so there isn't much need for this to be short, and also it is obvious to the reader what happens. This is also rather close to how template specializations work in C++ – which might or might not be a good thing :).

from nimyaml.

Graveflo avatar Graveflo commented on July 17, 2024

I'm not sure what you are trying to say with the first example. When instantiated, T is bound to Bar, yes but this has nothing to do with overload resolution. If you look at the manual on overload resolution: https://nim-lang.org/docs/manual.html#overload-resolution you will see that it is the "formal parameter" that is compared in order to determine which candidate is selected, not the instantiated type. Instantiation happens after candidate selection and it must be this way or it doesn't make any sense. If the instantiated type was used for determining specificity then all matching generics should be ambiguous with one another and there would essentially be no specialization of generics at all.
Generics and inheritance are very different, that is true, but they are both abstractions. Generics are symbolic abstractions that are evaluated at compile time and inheritance are data abstractions that support run time evaluation.
Consider:

type A[T] = object

proc p[T](x: T):string = "T"
proc p(x: object):string = "object"
proc p(x: A):string = "A"
proc p[T](x: A[T]):string = "A[T]"
proc p(x: A[SomeInteger]):string = "A[SomeInteger]"

echo p(A[int]())

These are all generic procs with different levels of specialization. If you supplied something like A[int]() in this scenario every one of these procs would match for it, because they can all instantiate that type, but they are not ambiguous in this example for obvious reasons. The candidate is selected before instantiation by comparing the formal parameter type (T, object, A, A[T], A[SomeInteger]).


If what you said is true, in that inheritance is used for specialization and generics are not meant for it, then isn't this an argument for inheritance beating generics in overload resolution? It doesn't make sense for generics to swallow every specialized case of any object by simply having an object formal parameter if that is not what it is used for. I don't agree that generics aren't used for specialization but it's just a question to get you thinking about your rational.

Finally, the last example you provided is just using concepts (which are evaluated like generics) to hack in the logic that inheritance already implicitly has into the precedence level of generics. So yes, this is an example of what I am trying to do.
When you take all of these things into consideration it is simpler then I think you are making it. You have candidates that define formal parameters and you have the types of the inputs. It's the job of overload resolution to pick the best matching candidate from the pool. Instantiation is a separate mechanism and specialization is used for both generics and inheritance. Right now inheritance is crippled by generics with the current rules and there is no reason for this from a design perspective.

from nimyaml.

flyx avatar flyx commented on July 17, 2024

but this has nothing to do with overload resolution.

My point was to show that it's generally wrong to assume a non-generic proc taking a parent type is a better match for overload resolution than a generic proc, which is to my understanding what you propose.

If what you said is true, in that inheritance is used for specialization and generics are not meant for it, then isn't this an argument for inheritance beating generics in overload resolution? It doesn't make sense for generics to swallow every specialized case of any object by simply having an object formal parameter if that is not what it is used for.

No, you're thinking about a generic function as if it were a non-generic function taking a RootObj parameter. My view is that it isn't, because generic instantiation can bind names in the generic body to specialized procs. And that's why my opinion is that the generic proc is rightfully beating inheritance.

Right now inheritance is crippled by generics with the current rules and there is no reason for this from a design perspective.

My experience is that I have never encountered this problem or, to my knowledge, worked around it without thinking much about it. I have a hard time envisioning use-cases for this that would show up somewhat regularly. So to sum up:

  • What you want to do is, according to my experience with different code bases and projects, an esoteric feature without many use-cases.
  • It is nevertheless already possible to implement with features that Nim supports today.
  • If it was implemented like you describe it, it would break code like the one in this project you asked about, since the non-generic option suddenly would take precedence over the generic one. Additional features would be needed to support my use-case.
  • Since it doesn't add a new feature that is really missing and few code bases will probably benefit from it but it would probably make the language more complex, I come to the conclusion that this isn't a good idea.

Then again, I'm not an active Nim user. I maintain this library because it's being used in the wild and that's it. I left Nim quite some time ago due to general disagreements with its design decisions so I'm certainly not someone whose opinion carries any weight and you're very welcome to disagree with me.

from nimyaml.

Graveflo avatar Graveflo commented on July 17, 2024

Yea that's alright. I like picking people's brain about this stuff because it matters to me. I'm not saying that inheritance should beat out generics, just that they should compete. The common use case or best fit example is the one from my initial post.
If I did somehow get this change implemented I would PR a fix to this repo since it would be trivial. The code breakages are by far the greatest deterrent for this as far as I know. Thanks for entertaining my questions though

from nimyaml.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.