Skip to content

Polymorphise the type of some functions #319

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

Merged
merged 1 commit into from
May 7, 2021

Conversation

aspiwack
Copy link
Member

@aspiwack aspiwack commented Mar 8, 2021

What I've done is take the prelude functions that we use all the time
and polymorphise the higher-order arrows.

I left the first order arrow to multiplicity 1 whenever possible
because:

  • We can cast them to any multiplicity by a simple 𝜂-expansion
  • Any multiplicity involving multiplicity multiplication will hit the
    limitations of the type checker currently (it doesn't know that
    multiplication is associative, typically), so I really wanted a
    single multiplicity variable per type.

This does leave composition fully linear, unfortunately, because there
are two (independent) higher-order arrow, and there is no most general
choice of one to polymorphise.

It didn't work last time I tried, but @monoidal made a cleverly short
patch
to
9.0 prior to release, and as he hinted to me last week, it does make
this polymorphisation possible.

Closes #309 .

@aspiwack aspiwack requested a review from utdemir March 8, 2021 17:31
@aspiwack aspiwack mentioned this pull request Mar 8, 2021
Copy link
Contributor

@utdemir utdemir left a comment

Choose a reason for hiding this comment

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

This is great! I also remember making them polymorphic was making type-checker confused, so it's nice that this got solved. The fact that the rest of the linear-base still compiles without any more annotations convince me that this is a good idea.

($!) f !a = f a

-- | Beware, 'curry' is not compatible with the standard one because it is
-- higher-order and we don't have multiplicity polymorphism yet.
curry :: ((a, b) %1-> c) %1-> a %1-> b %1-> c
curry :: ((a, b) %p-> c) %1-> a %p-> b %p-> c
curry f x y = f (x, y)

-- | Beware, 'uncurry' is not compatible with the standard one because it is
-- higher-order and we don't have multiplicity polymorphism yet.
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps this comment needs an update now that the function uses multiplicity polymorphism.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1. You can generalize the remaining linear argument for curry and then remove the comment, it will completely subsume the one in Prelude. I've tried

($) :: (a %p-> b) %q-> a %p-> b
(&) :: a %p-> (a %p-> b) %q-> b
const :: a %m-> b -> a
asTypeOf :: a %m-> a -> a
($!) :: (a %p-> b) %q-> a %p-> b
curry :: ((a, b) %p-> c) %q-> a %p-> b %p-> c
uncurry :: (a %p-> b %p-> c) %q-> (a, b) %p-> c
forget :: (a %1-> b) %m-> a -> b

and the testsuite passed. I think it's reasonable to expect this to work at this complexity in 9.0. If there is a place where the monomorphic version works but the polymorphic doesn't, please let me know.

@@ -19,13 +19,13 @@ import Data.Functor.Identity

-- | Beware: @($)@ is not compatible with the standard one because it is
-- higher-order and we don't have multiplicity polymorphism yet.
($) :: (a %1-> b) %1-> a %1-> b
($) :: (a %p-> b) %1-> a %p-> b
-- XXX: Temporary as `($)` should get its typing rule directly from the type
-- inference mechanism.
Copy link
Contributor

Choose a reason for hiding this comment

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

You can remove the comment per ghc-proposals/ghc-proposals#406

@aspiwack aspiwack force-pushed the polymorphise-prelude-a-little branch from 7f57079 to 01604c9 Compare May 7, 2021 13:41
@aspiwack
Copy link
Member Author

aspiwack commented May 7, 2021

I (finally!) came back and addressed the comments.

The reason why it's taken me that long is because I was hesitant to make all the first-order arrows multiplicity-polymorphic as @monoidal suggested.

Here's the thing: the fact that I can do that is due to the fact that we have a built-in rule in the compiler that every multiplicity is compatible with exactly-once consumption. This precludes some possible further multiplicities (like the dreaded 0, or a multiplicity saying “exactly twice”. While I don't really think that we will ever need a multiplicity “exactly twice”, I consider it healthy to make a compatible system).

So why do we have this rule? Well, we absolutely positively needed it for backward compatibility of the constructors (constructors are linear by default, but they are currently used as unrestricted functions in many places): constructors expose a polymorphic interface. Since we don't have a way, yet, to express a “p is compatible with exactly-once consumption” constraint (which I imagine will be spelt something like 'One <= p), we simply internalised it.

I was very hesitant to extend the use of this, which I consider to be a hack in the system beyond this internal use. On the other hand, we have a similar problem with this prelude functions as with the constructors, if we want the function to be a drop-in replacement. And so I agonised for weeks (and had several discussions with @monoidal off band).

He finally convinced me that it was the way to go and to embrace the hack. Basically, because we need the 'One <= p rule for the sake of compatibility, the only way we can add incompatible multiplicity is by adding such a constraint first. When that happens we can upgrade all our functions to use it.

I wouldn't advertise that people necessarily use this style except when backward compatibility like this is a prime concern, as it is code that may break whenever we figure out our story here. But for linear-base, it's almost certainly the way to go.

@aspiwack aspiwack force-pushed the polymorphise-prelude-a-little branch from 01604c9 to 6d39961 Compare May 7, 2021 13:54
What I've done is take the prelude functions that we use all the time
and polymorphise the higher-order arrows.

I left the first order arrow to multiplicity 1 whenever possible
because:
- We can cast them to any multiplicity by a simple 𝜂-expansion
- Any multiplicity involving multiplicity multiplication will hit the
  limitations of the type checker currently (it doesn't know that
  multiplication is associative, typically), so I really wanted a
  single multiplicity variable per type.

This does leave composition fully linear, unfortunately, because there
are two (independent) higher-order arrow, and there is no most general
choice of one to polymorphise.

It didn't work last time I tried, but @monoidal made a [cleverly short
patch](https://gitlab.haskell.org/ghc/ghc/-/merge_requests/4632) to
9.0 prior to release, and as he hinted to me last week, it does make
this polymorphisation possible.

Closes #309 .
@aspiwack aspiwack force-pushed the polymorphise-prelude-a-little branch from 6d39961 to f3aff2f Compare May 7, 2021 14:07
@aspiwack
Copy link
Member Author

aspiwack commented May 7, 2021

Mmm… so I had to inline a couple of things in the Hashmap tests where there were definition like:

let (#.) = (Linear..) in

The reason, I imagine, is that we are performing defaulting too eagerly in lets. It is not affected by -XMonoLocalBinds. I think that it's fine for now. I could have added a type signature to (#.) as well, but that felt unnecessary.

@aspiwack aspiwack merged commit 48757c9 into master May 7, 2021
@aspiwack aspiwack deleted the polymorphise-prelude-a-little branch May 7, 2021 14:17
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.

Generalize (.) ?
4 participants