Skip to content

Conversation

@purefunctor
Copy link
Member

@purefunctor purefunctor commented Nov 21, 2021

This PR implements type-level integers for PureScript and resolves #2899. This builds against latest master instead of #3058 as rebasing would be pretty tedious. Type-level integer literals would be kinded as the Int type so breakage should be fairly minimal, but it's probably best that this would be merged in a major release.

Here's an example of what this makes possible:

module Main where

import Prim.Int (class Add, class Mul, class Compare)
import Prim.Ordering (EQ)

data Proxy :: forall k. k -> Type
data Proxy n = Proxy

addInt :: forall x y z. Add x y z => Proxy x -> Proxy y -> Proxy z
addInt _ _ = Proxy

mulInt :: forall x y z. Mul x y z => Proxy x -> Proxy y -> Proxy z
mulInt _ _ = Proxy

compareInt :: forall x y z. Compare x y z => Proxy x -> Proxy y -> Proxy z
compareInt _ _ = Proxy

a :: Proxy 42
a = addInt (Proxy :: _ 21) (Proxy :: _ 21)

b :: Proxy 42
b = mulInt (Proxy :: _ 21) (Proxy :: _ 2)

c :: Proxy EQ
c = compareInt (Proxy :: _ 21) (Proxy :: _ 21)

For further discussion:

  • Pow/Root
  • Sub/Div/Mod
  • Int vs Integer for the backend. Having reflection means that Nats are bounded to the upper limit for JS, so the current Integer backend is a bit overkill for this.

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)

@MonoidMusician
Copy link
Contributor

Regarding the names: there was some discussion that it would be nice to actually rename Symbol to String (#3103) and have the same names at the type level, especially if we had datakinds at some point. That would be invasive, so we don't necessarily have to do that. But is it at least possible with polykinds/type-in-type now? Can we create one Prim.Natural and have it work at the term and type levels? If so, maybe we could consider adopting term-level Natural into Prelude, with a very small API, and deprecating other packages that define it.

I suppose the problem is that we don't have dedicated natural literals at the term level, so it quickly gets to be a lot of work, sorry.

Copy link
Contributor

@JordanMartinez JordanMartinez left a comment

Choose a reason for hiding this comment

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

Just some initial comments as I looked through this.

@purefunctor
Copy link
Member Author

@MonoidMusician So, if I understand correctly, we can introduce term-level Naturals to mirror the relationship between String and Symbol, right? e.g. Prim introduces the Natural type, then prelude/natural exposes Data.Natural and typelevel-prelude exposes Type.Data.Natural.

It's definitely quite a lot of work but I do think that this would be pretty nice.

@rhendric
Copy link
Member

I don't see the point of dedicated Sub, Div, Mod classes when these things could just be implemented with functional dependencies on Add and Mul. Am I wrong about that, or is there a reason the former would be better?

More broadly, and probably more controversially, should we be adding new type-level literals ad-hoc like this? As @MonoidMusician reminded us, we're already possibly regretting the separation between Symbol and String. I wonder if in the future we'll want to support all PS literals at the type level, and have a primitive class like

class IsReflectable t v | v -> t where
  reflectType :: forall proxy. proxy v -> t

which would subsume both IsSymbol and IsNat. We'd also be wrestling with the Int versus Nat distinction there.

I bring this up because, if the compiler is going to implement Add and Compare, do we lose anything by using type-level Ints instead of a new Nat kind? I know this was previously discussed here. I'm not strongly swayed by the counterarguments there; they may well be correct, but I might need to see a worked example of something that is made worse or impossible by using type-level ints with appropriate constraints instead of type-level nats. If we can get away with type-level ints, then maybe we should go straight to IsReflectable instead of IsInt, even if we don't implement other type-level literals right now.

@purefunctor
Copy link
Member Author

purefunctor commented Nov 23, 2021

I don't see the point of dedicated Sub, Div, Mod classes when these things could just be implemented with functional dependencies on Add and Mul. Am I wrong about that, or is there a reason the former would be better?

Personally, I think that having separate type classes for these operations is much more intuitive than having different fundeps provide different solutions. Row.Cons, for example, is an example of a pretty overloaded type class whose name doesn't really help make its purpose understandable. If we could rename Add/Mul to something that reflects that it can also solve Sub/Div, I'd definitely be more inclined towards this.

I wonder if in the future we'll want to support all PS literals at the type level [...]

We already 2/5 of the non-container literals at the type-level (Boolean, String/Symbol), and so far we've only really gotten it "right" with Boolean. If it is an inevitability, then yeah, I do agree that IsReflectable is what we should go with, to at least help ease future implementations (see also #3102).

[...] do we lose anything by using type-level Ints instead of a new Nat kind? [...]

For use-cases that require non-negative numbers, I agree that Compare is a good compromise over losing the convenience of intrinsically only having natural numbers.

@JordanMartinez
Copy link
Contributor

Am I wrong about that, or is there a reason the former would be better?

I'm not sure, but how would you implement Sub using just Add with only one functional dependency of l r -> o, which corresponds to l + r == o? Would you recursively search what the values of r are (e.g. l + 1, l + 2, etc.) until you found a value that worked?

@rhendric
Copy link
Member

No no, in a Sub-less world, Add would go back to having l r -> o, l o -> r, r o -> l fundeps, and each one would be solved by the compiler with the appropriate expression (o = l + r, r = o - l, l = o - r respectively). For Mul, and for Add as well if we stick to nats and not ints, some constraints will be rejected instead if the result is not a member of the expected domain (Mul 5 r 17, for example, should not have an instance).

@JordanMartinez
Copy link
Contributor

I know I already said it, but I'm for Reflectable

By NonZero, you mean something like this?

class NonZero :: Int -> Constraint
class NonZero i

instance Fail (Text "Got a zero!") => NonZero 0
else instance NonZero (i :: Int)

Could the same thing be achieved via an IsZero constraint?

class IsZero :: Int -> Boolean -> Constraint
class IsZero i b | i -> b

class (IsZero i False) <= DivMod ...

@rhendric
Copy link
Member

rhendric commented Feb 14, 2022

Re NonZero etc.: the question is what does any of this get us? DivMod is not going to be implemented by users, so the only point of giving it superclasses is making those superclass instances available in any context where you have a DivMod. If these superclasses are memberless, then the only point of having instances of them is if people design APIs that consume these instances. So show me the designs for APIs that consume NonZero etc.

My reluctance for including DivMod, as I tried to explain in a previous comment, is that there are lots of relationships that are true and expressible just with Compare between numerator, denominator, quotient, and remainder, but only if the sign of the denominator is known. To wit:

  • if d > 0, then d > r >= 0
  • if d < 0, then d < r <= 0
  • if d > 0, then q < 0 iff n < 0
  • if d < 0, then q < 0 iff n > 0

Our options are:

  1. ignore these relationships, which I think will hinder the utility of DivMod, but that depends on the use cases
  2. constrain DivMod to require the denominator, and possibly even the numerator, to be positive, which may also hinder the utility of DivMod but in a different way, again depending on the use cases (but also frees us from having to think about NonZero as a special case)
  3. come up with even more type-level stuff to capture the conditionality of these relationships, which I think is probably too speculative to be worth it

The reason I keep saying to hold off on DivMod until we have use cases is that I want to be able to distinguish between 1 and 2. Once we figure out which path is more useful, then we'll know whether we want a NonZero-like thing or not, and then we can bikeshed about how it works.

@JordanMartinez
Copy link
Contributor

then the only point of having instances of them is if people design APIs that consume these instances. So show me the designs for APIs that consume NonZero etc.

Good point.

Correct me if I'm wrong, but can't DivMod be added in the future as a non-breaking change? If that's the case, then I'd agree that excluding it for the time being is worth doing. If this PR (without DivMod) gets included in a release, then we'll likely run into more design issues that DivMod would fix besides those mentioned in the Discourse post.

@purefunctor
Copy link
Member Author

By NonZero, you mean something like this?

Yes, although I haven't added solving for the NonZero constraint yet, it only really exists to constrain DivMod; refactoring to IsZero should also be fairly trivial.

but can't DivMod be added in the future as a non-breaking change?

I'd be more than happy to do this as well. I think having a dedicated issue for it would flesh out the implementation better.

Correct me if I'm wrong, but this should be the final sets of primary changes right? I hope I'm not forgetting anything that was discussed a few weeks back 😄.

@JordanMartinez
Copy link
Contributor

JordanMartinez commented Feb 15, 2022

So, have we reached consensus that the following changes need to be made before this PR will be merged?

  • Rename IsReflectable to Reflectable
  • Drop DivMod and NonZero

This allows the type-level Boolean and Ordering types to be solved.
Copy link
Contributor

@JordanMartinez JordanMartinez left a comment

Choose a reason for hiding this comment

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

LGTM!
I marked this PR as a breaking change, but I can't recall why. I think it's only breaking to users of the compiler as a library, but not breaking like ES modules breaking.

@purefunctor
Copy link
Member Author

@JordanMartinez I believe it was solely because of the introduction of the Nat type, which is used by some packages.

@purefunctor
Copy link
Member Author

I'll be merging master to resolve the conflict here

@JordanMartinez
Copy link
Contributor

Looks like there's another merge conflict due to #4239.

Copy link
Member

@rhendric rhendric left a comment

Choose a reason for hiding this comment

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

Other changes all look good—I think if you just make that one case expression unconditional and add an appropriate test, I can approve this.

This fixes a bug where arithmetic facts aren't taken into account when
solving nominal `Compare` constraints. The test included in this commit
exercises this change, whereas previously, it'd fail when solving
`Compare a 10 LT`.
@purefunctor
Copy link
Member Author

Test failures are related to the bundler because of 516abc2, it seems like. Tests for type-level ints are all passing.

@rhendric
Copy link
Member

rhendric commented Mar 3, 2022

Yes, sorry, these failures are indeed not your fault. Because ES module support is currently in development, the tests currently depend on some temporary forks of PureScript libraries that have been changed to be compatible with the new module format, and those forks are getting some cowboy commits at the moment that are breaking everyone else. This will affect local and CI builds (if your local build isn't affected yet, it's because your cached test dependencies haven't expired yet—you have at most a day before this affects you). Please stand by while we restore order.

@JordanMartinez
Copy link
Contributor

CI builds now.

Copy link
Member

@rhendric rhendric left a comment

Choose a reason for hiding this comment

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

Nice work.

Co-authored-by: Ryan Hendrickson <ryan.hendrickson@alum.mit.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add type-level natural numbers

6 participants