
On Sat, Apr 17, 2021 at 12:29 PM Carl Meyer <carl@oddbird.net> wrote:
On Fri, Apr 16, 2021 at 11:00 AM Jukka Lehtosalo <jlehtosalo@gmail.com> wrote:
Note that the results here are usable in runtime contexts, since we call the function:
x = N(2) # Ok
My main question about the details of your proposal is this piece. Partial laziness adds a lot of complexity both to implementation and simply to understanding the semantics; is it really necessary? Might it not be acceptable to say that `x = N(2)` can only occur after any forward references involved in the definition of `N` have been defined? (In the common cases I think it would occur inside functions; the ordering is only really an issue if it occurs at top level.) And if so, could we avoid the complexities of partial laziness and instead just always wrap the entire RHS in lazy evaluation?
The partial laziness seems helpful to avoid explicit escaping in use cases like this: type T = TypeVar(bound=D) class C(Generic[T]): ... # If we evaluate T fully, we get NameError about D class D: ... Code like this seems somewhat common. I could find over 100 forward references in type variable bounds in a large internal codebase. Not having partial laziness could result in getting a NameError potentially far away from the source, which can be confusing. Changing the order of definitions will sometimes help, but it's not always practical. It can create a lot of needless code churn, especially if we are talking about changing the order of big classes. Some forward references are caused by import cycles, and avoiding import cycles is often not possible without major compromises. If we'd limit the use of forward references, users might prefer the old way as more flexible. There are other possible tricky situations that involve base classes. Example: type NT = NamedTuple(x=D) class class C(NT): ... class D: ... Occasionally type objects are also stored in some module-level data structures. Hypothetical example: type X = NamedTuple(...) type Y = NamedTuple(...) dd = { 'key1': X, 'key2': Y, ... } The other issue is implicitly passing the stringified name as first arg.
Personally I would rather handle the lazy evaluation in a generalized way as something like “module level descriptors”, in which case the name issue could be handled via the existing `__set_name__` protocol instead.
I don't much like the magic first argument, to be honest. Something like an implicit call to '__set_name__' sounds better to me. I'd just like to be able to drop the explicit string literal name argument somehow, since it looks quite awkward -- especially if the type name is long. Jukka