-
Notifications
You must be signed in to change notification settings - Fork 562
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
Feature: local type synonyms #3708
Comments
I see I've collected a number of 👍s but no discussion. I've been keeping #3709 up to date when conflicts arise; is there anything else I can do to move this proposal along? |
Sorry, but there isn’t really - I think it’s mostly that this is more of a ‘nice to have’ feature, and in particular it doesn’t enable anything which is not currently possible, which makes it difficult to justify prioritising maintainer time for it over certain other things (like, say, poly kinds, or getting try purescript updated, which I think is where most maintainer time has been going recently). |
Those are indeed more worthy uses of maintainer time, if such time must be allocated exclusively to one or the other. I'll only ask that if it becomes feasible to take a few minutes to spin off some tasks—theoretical investigation, alternate implementations, whatever—that a non-maintainer can work on, please let me know! |
I'm not opposed to this, but is there a significant advantage to desugaring to top-level declarations with name munging, vs just supporting them in the type checker under it's normal scoping? While it's true that it can be desugared this way (as a way to say it's equivalent to anything you can write now), it's not clear to me if this is actually advantageous or simpler than supporting it for "real". |
Not necessarily. I do think it's simpler to specify as a desugaring, but to me, the main advantages of implementing this as a desugaring are sidestepping the anticipated objection against ‘future additions of type-level features’ and having more confidence that the patch doesn't introduce bugs into existing language features. Supporting type synonym scopes ‘natively’ probably would be less net complexity added than doing it with name mangling, if complexity in a desugaring step and complexity in the type checker core is weighted equally. But I'm assuming the maintainers take the view that complexity in the type checker core ought to have more weight—whether enough weight to justify some name mangling hacks, I can't say. |
Speaking from the perspective of |
I'd like to at least consider the question of which kinds of declarations we want to allow to be 'local' and which we don't. Right now it's just value declarations that can exist within a I'm also just a little concerned that this might complicate the work we'd like to do soonish to make type synonyms appear in error messages where appropriate, because we'd probably need to make sure that local type synonyms only appear in errors which arise somewhere where they are in scope, and outside of that scope they're expanded. But that's probably surmountable? Perhaps this suggests that it would be better to implement type synonym scopes 'natively' inside the type checker after all? |
@kritzcreek would you have concerns with the IDE if this were implemented 'natively' inside the type checker, i.e. without the hoisting and name mangling? |
Forgive me, I know nothing about
Yeah, I largely agree with your conclusions. Local The motivating story for local I've done some idle speculation about a world with all of local Bottom line, I don't think there's much potential for nontrivial interaction between local |
Yeah the result wouldn't change. The IDE indexes and queries top-level identifiers. It doesn't do it on a per definition granularity.
We don't do anything for local value declarations, that's what I'm saying. When I say net-negative I don't mean that things that used to work will stop working, but where you could previously go-to-definition, show kind etc. on every type since they're always toplevel, it's now only going to work for a subset of the types in your program. That's the net-negative |
@natefaubion raised some polykinds-related questions over in #3709; I'd like to discuss them here. The initial proposal assumed a context where unannotated type parameters in foo :: forall f. Proxy f -> Int
foo _ = 0
where
type LocalApp a = f a Per the proposal as currently written, this would be equivalent to type HoistedLocalApp f a = f a
foo :: forall f. Proxy f -> Int
foo _ = 0 which leads to a generalized type warning. After consideration, I think the best thing to do is to behave as if a foo :: forall f. Proxy f -> Int
foo _ = 0
where
test = Proxy :: Proxy (f Int) This means issuing no warnings, and making sure there is a unification error if I don't know if this is quite fatal for the desugaring approach, but it does seem to complicate matters. The saving grace might be that, weird pointless test cases aside, any practical real-world application would give the compiler enough information to infer the correct kind when the generalized hoisted local type is used. By which I mean, as soon as you actually have to use foo :: forall f. Proxy f -> Int
foo _ = 0
where
type LocalApp a = f a
test = Proxy :: Proxy (LocalApp Int) the compiler now infers that So the question may be, how much do we care about pointless test cases where local type synonyms are declared but not used? If the answer is not at all, then I think desugaring otherwise behaves correctly as long as the generalization warning is suppressed for the prepended parameters in the hoisted type synonyms. Anyone else have any thoughts? |
Something else to consider is scoped type variables within kind signatures of local type synonyms. foo :: forall k (a :: k). Proxy a -> ...
foo _ = ...
where
type Foo (b :: k) = ... |
And by ‘consider’ you mean ‘support’, right? There would be no reason not to want that to work, would there? |
Yes, it is something I would expect to work with PolyKinds. Kinds and types are in a unified namespace, so scoped type variables apply. This is tricky to do with desugaring steps. For example, scoped type variables and PolyKinds don't currently work together in class declaration bodies because the desugaring step happens before kind checking, so there is no (easy) way to tie the scope on the original class declaration to the desugared record synonym. Fixing this means delaying desugaring to when it is checked. For reference, I think in order to support this in the type checker, one would need to:
I don't think anything else would be necessary (however there can always be subtle cases to handle). I think that generalization should be avoided, because I think we should have a consistent rule for when it happens:
|
Just to be clear: when you propose that a let-bound type synonym not be generalized, do you mean that unconstrained type parameters have their kinds inferred from one use of the type synonym in the scope, the same way that local functions have their parameter types inferred? Or do you mean that unconstrained type parameters have kind |
The first. I don't think we should |
I think this is a great addition. This seems to be related to also allowing fun
:: forall a b.
let A = {someLongFieldName :: a, anotherLongFieldName :: b}
B = Either a b
C = Tuple a b
=> C -> B -> A |
It's been some time since this has been discussed. I think I'm for supporting this. There's been a few places where a local type synonym would clean up code in some |
Summary
This is a proposal to support locally scoped type synonyms inside
where
andlet
binding lists, with the obvious syntax and semantics.(Edit: This post was originally written before the polykinds feature was merged. The proposal as previously written didn't interact well with that feature; it has been rewritten to reflect the current consensus based on those interactions, as arrived at in this issue and in associated PR comment chains. Comments here about desugaring by hoisting local type synonyms to the top level are in reference to the original proposal.)
Motivation
There are a few reasons why one might want to define a local type synonym. The simplest is locality itself: sometimes it's desirable to bundle up one or two dozen functions into a ‘submodule‘ consisting of an outer function with a long list of
where
bindings. If those inner functions reuse a complicated type a few times such that a type synonym would be useful, a synonym defined near to where it's used is more legible than one that's out in the top-level scope, possibly off-screen. Another reason is brevity: a top-level type synonym demands a name that is unambiguous in the context of the entire module, whereas a type synonym scoped only to a single definition can be more succinct without clogging up the outer namespace. My favorite reason, though, is that defining type synonyms for types involving type variables requires you to parameterize the type synonym as well... unless it shares a scope with the type variable. Consider the difference between these two snippets—IMO, the readability gap is obvious, and continues to widen as you imagine the number of type variables involved growing:(All of these reasons have their analogs in terms of why
let
/where
bindings for values are useful even though we have top-level value declarations—in principle, many inner bindings could be replaced with a top-level declaration that is likely to suffer from at least one of: being farther away, having a longer name, or needing more parameters. But why suffer?)Proposal
Syntactically, make
type
declarations and their associated kind signatures legal inside the binding lists oflet
expressions,where
clauses, and anything parsed similarly. Semantically, these synonyms introduce new types in the same lexical scope as a value binding in the same position. Like otherlet
bindings, local type synonyms can't be exported or used outside of their scope (which does not include the type signature of the value containing the binding—top-level values must be annotated with types visible from the top-level scope), and like regular type synonyms, they can't have instances or cyclic definitions, and their lexical position within the binding list is irrelevant.Previously, this proposal defined the semantics of local type synonyms in terms of a desugaring transformation; in light of the polykinds feature, this doesn't seem to be the clearest way to express or implement the desired behavior. Instead, I'll just wave my hands a little and say that local type synonyms have the same semantics as top-level type synonyms, with the following differences:
forall
in the type signatures of enclosing value declarations, the same way that a type annotation in the same position can.forall
appearing in a local type synonym or its kind signature introduces a type variable with the same name as a type variable in scope at the local type synonym's position, the inner type variable shadows the outer one within the scope of itsforall
.UnusedName
(added in Unused warnings #3819) warning if it is not used (not anUnusedDeclaration
, which will only apply to top-level declarations).This proposal neither includes nor precludes the following potential future extensions/analogous features:
Examples
I have an only-mildly-hackish implementation of this to be offered as a PR shortly; I've been using it locally for a week and this feature has really helped me clean up some nasty signatures.
The text was updated successfully, but these errors were encountered: