Skip to content
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

"Annotation vs. Internal Annotation" is essentially variable shadowing (and the solution is to disallow it) #2332

Open
mskiptr opened this issue Aug 7, 2024 · 2 comments

Comments

@mskiptr
Copy link

mskiptr commented Aug 7, 2024

This is something to consider for the next release.

While browsing the documentation I encountered this paragraph:

Annotation vs. Internal Annotation

A quite tricky case is when an outer type annotation clashes with an inner type annotation. Here is an example of this:

filter : (a -> Bool) -> List a -> List a
filter isOkay list =
  let
    keepIfOkay : a -> Maybe a
    keepIfOkay x =
      if isOkay x then Just x else Nothing
  in
    List.filterMap keepIfOkay list

This case is very unfortunate because all the type annotations are correct, but there is a detail of how type inference works right now that user-defined type variables are not shared between annotations. This can lead to probably the worst type error messages we have because the problem here is that a in the outer annotation does not equal a in the inner annotation.

For now the best route is to leave off the inner annotation. It is unfortunate, and hopefully we will be able to do a nicer thing in future releases.

Here, the top-level type annotation implicitly binds the name a, and uses it three times to construct a type. To make what this means slightly clearer, here are examples in Haskell and Idris that do so explicitly:

  • filter :: forall a. (a -> Bool) -> [a] -> [a]
  • filter : {a : Type} -> (a -> Bool) -> List a -> List a

At call site, a concrete value of a is passed as a hidden argument to filter and then used to instantiate a concrete type signature (like (Int -> Bool) -> List Int -> List Int).

The problem, as described in the above snippet, is that the a used in the inner type annotation is a brand new a. In Haskell binding a variable explicitly disables shadowing and lets us re-use it in any nested definitions while in Idris this a is readily available in the entire body of filter even when the binding is left implicit.

A good solution would be to let the (implicitly-bound) type variables be available in any nested type annotations and not do any variable shadowing at the type level. In case the user truly wants a new type variable, they can just use a different name. This would of course be a breaking change, but I think doing so to get rid of confusing behavior is absolutely worth it.

The only potential problem I see is that the opposite situation might become confusing. But that depends on whether we can have a good error message there. (I guess an if (a type variable is re-used) and (there is a type unification error with it) then (warn about shadowing) might be a good heuristic.)

Copy link

github-actions bot commented Aug 7, 2024

Thanks for reporting this! To set expectations:

  • Issues are reviewed in batches, so it can take some time to get a response.
  • Ask questions in a community forum. You will get an answer quicker that way!
  • If you experience something similar, open a new issue. We like duplicates.

Finally, please be patient with the core team. They are trying their best with limited resources.

@lue-bird
Copy link

lue-bird commented Aug 7, 2024

I'm pretty sure this paragraph is just outdated as the snippet compiles just fine: https://ellie-app.com/rTqZCjxYyk8a1

The solution seems to have been exactly as you suggested: re-using outer type variables if the name is the same

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants