Skip to content

Visible type applications#4235

Closed
purefunctor wants to merge 98 commits intopurescript:masterfrom
vtrl:visible-type-applications
Closed

Visible type applications#4235
purefunctor wants to merge 98 commits intopurescript:masterfrom
vtrl:visible-type-applications

Conversation

@purefunctor
Copy link
Copy Markdown
Member

@purefunctor purefunctor commented Feb 15, 2022

Closes #3137. This PR adds support for visible type applications and abstractions. The latter pertains to syntax for declaring which universally quantified variables can be instantiated with the former. Here's a simple example:

id :: forall @a. a -> a
id a = a

id' :: Int -> Int
id' = id @Int

Unlike GHC's TypeApplications, polytypes with variables that aren't prefixed with @ cannot be used for visible type application:

Error found:
in module Main
at Main.purs:6:7 - 6:14 (line 6, column 7 - line 6, column 14)

  An expression of type:
                    
    forall a. a -> a
                    
  cannot be applied to the type:
       
    Int
       

while inferring the type of id
in value declaration id'

See https://github.com/purescript/documentation/blob/master/errors/CannotApplyExpressionOfTypeOnType.md for more information,
or to contribute content related to this error.

Visible type abstractions can also appear in class heads, like so:

class Functor @f where
  map :: forall a b. (a -> b) -> (f a -> f b)

mapArray = map @Array

Checklist:

  • Added a file to CHANGELOG.d for this PR (see CHANGELOG.d/README.md)
  • [-] Added myself to CONTRIBUTORS.md (if this is my first contribution)
  • Linked any existing issues or proposals that this pull request should close
  • Updated or added relevant documentation
  • Added a test for the contribution (if applicable)

This commit starts work on visible type applications. This change in
particular adds support for parsing '@'-prefixed type variables that are
consumed by visible type applications. They can also be referred to as
VtaTypeVars to distinguish them from TypeVars.

Future Work:
* Fix up pattern matching for the `TypeVarBinding` type.
* Add representation and conversion for VtaTypeVars on the `AST`.
Positions.hs
* Recognize '@' in typeVarBindingRange

Flatten.hs
* Recognize '@' in flattenTypeVarBinding

Utils.hs
* Add ignore patterns for TypeVarBinding

Convert.hs
* Add ignore patterns for TypeVarBinding
Convert.hs
* Fix up conversion to ForAll.

Types.hs
* Create the VtaForAll type with Aeson instances.
* Insert VtaForAll as a field to the ForAll constructor.
* Fix up pattern matching for ForAll. Note that NotVtaForAll is
  preferred as a "default", especially for internal utilities.

Environment.hs
* Fix up pattern matching for ForAll. Also add the tyForAllVta
  constructor.
This fixes a bug in the kind inference rule where the VtaForAll field
is accidentally overwritten by NotVtaForAll.
This fixes a bug where Prim names aren't desugared in VisibleTypeApp,
which omits their quantification, making them not unify properly.
This makes visible type applications left-associative, which also
enables them to be used alongside function applications, like so:

```haskell
module Main where

data Tuple a b = Tuple a b

v2 :: forall @A @b. a -> b -> Tupla a b
v2 = Tuple

v2' :: Tuple Int Int
v2' = v2 @int @int 21 42
```
@purefunctor
Copy link
Copy Markdown
Member Author

CI errors seem to be related to JSON Encoding/Decoding as I'd added a field to the ForAll constructor. I'm not sure how to resolve this but maybe we could ignore them for now?

@purefunctor
Copy link
Copy Markdown
Member Author

I've started working on having visible type abstractions available for type class members like so:

class Functor f where
  map :: forall @f a b. (a -> b) -> (f a -> f b)

Although attempting to write an instance ends up with an error:

data Id a = Id a

instance Functor Id where
  map f (Id a) = Id (f a)

emits:

Error found:
in module Main
at Main.purs:9:10 - 9:14 (line 9, column 10 - line 9, column 14)

  Could not match type
      
    Id
      
  with type
      
    f0
      

while trying to match type Id t2
  with type f0 a3
while checking that expression case f $0 of          
                                 f (Id a) -> Id (f a)
  has type f0 b1
in value declaration $functorId1

where f0 is a rigid type variable
        bound at (line 0, column 0 - line 0, column 0)
      a3 is a rigid type variable
        bound at (line 0, column 0 - line 0, column 0)
      b1 is a rigid type variable
        bound at (line 0, column 0 - line 0, column 0)
      t2 is an unknown type

See https://github.com/purescript/documentation/blob/master/errors/TypesDoNotUnify.md for more information,
or to contribute content related to this error.

If anyone could point me to the right direction regarding this I'd be more than delighted with the help!

@purefunctor purefunctor marked this pull request as ready for review February 15, 2022 12:39
@JordanMartinez
Copy link
Copy Markdown
Contributor

I'm not familiar with how type applications are implemented, so I'm just thinking out loud. Doesn't that error make sense? f is a type variable that's introduced at the class head, not in the class' member. So, shouldn't it be more like this?

forall @f. class Functor f where
  map :: forall a b. (a -> b) -> f a -> f b

and then it's like

map2 :: forall @f. Functor f => (a -> b) -> f (f a) -> f (f b)
map2 = map <<< map

map2 @Array show

@purefunctor
Copy link
Copy Markdown
Member Author

@JordanMartinez That's correct, although the goal is to have @-vars not shadow vars in the class head, which I'm trying to figure out. Alternatively, we could also probably do the following syntax

class Functor @f where
  map :: forall a b. (a -> b) -> (f a -> f b)

And this would work just fine with:

mapArray = map @Array

On the other hand, could we do this for data constructors as well?

data Either @a @b = Left a | Right b

left = Left @(Proxy "this binds to b") 0

right = Right @(Proxy "this binds to a") 0

maybeLeft = Left @(Proxy "this binds to a") @(Proxy "this binds to b")

maybeRight = Right @(Proxy "this binds to a") @(Proxy "this binds to b")

@purefunctor
Copy link
Copy Markdown
Member Author

A bit more commentary on why the visible type abstraction on the class member fails: as the map declaration is type-checked, it's already inside an instance dictionary except f hasn't been replaced with the appropriate type yet. As understand it, at this point in the type-checking pipeline f would've been replaced with an unknown or with whatever fills in F in the instance, however, @f blocks this because of shadowing.

val' <- if checkType
then withScopedTypeVars mn args $ bindNames dict $ check val ty'
else return (TypedValue' False val ty')

The alternative syntax that I've proposed would likely be more trivial to implement as it doesn't interfere with said type-checking routines. All that's needed to be done is for the map method brought into scope to be turned from:

map :: forall f a b. (a -> b) -> (f a -> f b)

-- into

map :: forall @f a b. (a -> b) -> (f a -> f b)

Alternatively, there's also the choice of making @-annotations for class members completely decorative, eliminating them before type checking, and then restoring them right after, although I'm not sure if it's possible to only do this for ValueDeclarations that also happen to be class methods.

@purefunctor
Copy link
Copy Markdown
Member Author

purefunctor commented Jul 31, 2022

Because of the new semantics for visible type abstractions in type classes, I've removed the UnusableDeclaration error and replaced it with the OnlyPartiallyDetermined warning. It'll fire under the following conditions:

  1. The type class is not empty
  2. A type variable does not occur in any of its members
  3. A type variable is not determined by a functional dependency

Also the changelog entry for this feature is getting a bit long and more tutorial-like. I plan on writing an article for my blog in the future, as this feature is released. Would linking to that be better?

@MonoidMusician
Copy link
Copy Markdown
Contributor

I'm okay with the length of the changelog, since it's basically covering the various usages. It's not optional information.

I'll need to give more thought to the design. Definitely want to help move this along and make sure it's going to work well for users down the road.

My initial question is:
Is there any way to make different members have different visibility on type arguments? It might be nice for the case that only some members need a specific type variable. (I've definitely run into cases like that, where I either had to split it into different classes or add a proxy argument to one member, even though I initially thought it all fit into one class conceptually.)

Thinking about it a bit more, I'm not sure I like the idea of the required type arguments as opposed to optional ones. On its own it seems fine, but how it interacts with other features is less clear (e.g. my question above). Like how will we print them differently from non-required type arguments? What if you alias a method with an ordinary declaration – can it still have a required type argument?

Is it even necessary? Maybe we're trying to do too much work for the user with the feature. We already have AmbiguousTypeVariables and will still need to have it for inferred types. Do you think it's okay if users just see that error instead of a specialized error that the particular type application needs to be applied? Maybe we can do some more work to suggest where the type application would need to happen with that error, so we aren't losing specificity.

Of course it's okay to leave some stuff for future work, but ideally we can plan ahead so that it won't require breaking changes.

@MonoidMusician
Copy link
Copy Markdown
Contributor

Potentially add kind application syntax for instances, such that:

As for this, I think it's a bit out of scope, no? The original goal for this was to have type applications for expressions, and I've yet to refresh myself on what was discussed #3137 to see if there's anything to be added retroactively.

It would be great if we could support how things are printed currently, since you are doing that for polykinded types it would be great to also do it for polykinded instances. Lmk if you want help hacking on that, I don't anticipate too much difficulty given what you have already?

@MonoidMusician
Copy link
Copy Markdown
Contributor

There's something funny going on with constructors:

> :t Tuple
forall (a1 :: Type) (b2 :: Type). a1 -> b2 -> Tuple a1 b2

Printing their type doesn't include the vta quantification, even though they obviously support it!

I would offer to fix it but I haven't found the right place yet :)

@purefunctor
Copy link
Copy Markdown
Member Author

There's something funny going on with constructors:

> :t Tuple
forall (a1 :: Type) (b2 :: Type). a1 -> b2 -> Tuple a1 b2

Printing their type doesn't include the vta quantification, even though they obviously support it!

I would offer to fix it but I haven't found the right place yet :)

I think this is because of how constructors are monomorphized and then generalized back, which causes the loss of information. This was actually something that I had to get around with the Specified type approach.

@purefunctor
Copy link
Copy Markdown
Member Author

Is there any way to make different members have different visibility on type arguments? It might be nice for the case that only some members need a specific type variable. (I've definitely run into cases like that, where I either had to split it into different classes or add a proxy argument to one member, even though I initially thought it all fit into one class conceptually.)

Not at the moment, no. If we do support it though, what do you imagine would the syntax look like? I've tried having them on the members before as sort of an "override", like this, but this is potentially confusing.

class A a where
  a1 :: forall @a. a -> Int
  a2 :: a -> Int

Thinking about it a bit more, I'm not sure I like the idea of the required type arguments as opposed to optional ones. On its own it seems fine, but how it interacts with other features is less clear (e.g. my question above). Like how will we print them differently from non-required type arguments? What if you alias a method with an ordinary declaration – can it still have a required type argument?

This is indeed something to consider. The implementation is definitely designed around just accepting loss of information, for example, skipping is implemented as just losing the visibility for a type argument; aliasing too, would have certain implications. For what it's worth, GHC tries to be a bit smarter in that it keeps track of which unification variables should generalize into visible arguments—but then, should we follow GHC? 😄

Is it even necessary? Maybe we're trying to do too much work for the user with the feature. We already have AmbiguousTypeVariables and will still need to have it for inferred types. Do you think it's okay if users just see that error instead of a specialized error that the particular type application needs to be applied? Maybe we can do some more work to suggest where the type application would need to happen with that error, so we aren't losing specificity.

For what it's worth, expanding AmbiguousTypeVariables to know whether or not it was invoked due to type variables being skipped is something I've considered doing before OnlyPartiallyDetermined. Also, type arguments being visible by default in class heads with no way to opt out is what brought about the removal of UnusableDeclaration.

With the implications in mind, it shouldn't be too hard making visibility opt-in for class heads. So this would still throw an UnusableDeclaration:

class A a where
  a :: String

While this won't throw anything:

class A @a where
  a :: String

Until it's skipped, in which AmbiguousTypeVariables is thrown:

x = a @_

@purefunctor
Copy link
Copy Markdown
Member Author

The most recent changeset makes use of #4376. This brings the implementation closer to what is used by GHC, as only the ForAll type is involved as opposed to an extraneous Specified type.

Comment on lines +566 to +578
-- Collect all free type variables in visKnd, such that
-- quantifiers in bfrVis can be partitioned by whether
-- or not they appear within visKnd.
--
-- Given:
--
-- forall (j :: Type) (k :: Type) (@t :: k -> Type)
--
-- Then:
--
-- 1. freeInVisKnd: { k }
-- 2. bfrVisY: [ k ]
-- 3. bfrVisN: [ j ]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't take into account fringe cases such as:

forall (a :: Type) (b :: a) (@c :: Proxy b -> Type)

Where a also has to be instantiated alongside b. I doubt the compiler is ever able to synthesize such a type signature in most regular usage.

If there ever is a need for this, one way to go about it is to build a directed graph to figure out which variables need to be instantiated, starting from the kind of the visible quantifier. For example:

forall (a :: Type) (e :: Type) (b :: a) (c :: a -> Proxy b) (@d :: Proxy c)

Yields something along the lines of:

[ (d, [c]), (c, [a, b]), (b, [a]), (a, []) (e, []) ]

All that's left is to create the unification variables and replace as necessary.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be inlined into the code rather than this issue, then?

@purefunctor purefunctor modified the milestones: v0.15.4, v0.16.0 Oct 14, 2022
@purefunctor purefunctor mentioned this pull request Feb 16, 2023
5 tasks
@i-am-the-slime
Copy link
Copy Markdown
Contributor

I suppose this PR can be closed, right?

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

Successfully merging this pull request may close these issues.

Visible type applications

7 participants