-
Notifications
You must be signed in to change notification settings - Fork 154
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
Make operations on Index only unsafe by choice (#680). #688
Conversation
344c68c
to
b10c3b7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not super sold on the omnipresent KnownSatMode
. My guess is that it complicates error message /usage too much. I haven't had the time to experiment with it, so I don't know whether it's a necessity or not. I think I'd rather see something like:
instance Num (Index 'SatSymmetric n) [..]
instance Num (Index 'SatZero n) [..]
where
satAdd SatZero (2 :: Index 'SatWrap 3) 1
would force a certain saturation mode, but where otherwise "normal" operations would act like the type indicates. That is, given
a :: Index 'SatWrap 3
a = ..
=>
a + 1
~ satAdd SatWrap a 1
This would require the ability to write:
f :: Num (Index sat n) => ...
for some functions. I'm not sure if we can do that though. If we can though, we would eliminate the need for GADTs, which would make the API a bit more friendly to beginners.
I'll see if I can setup a proof of concept this week.
Oh, and thanks for making yet another PR. You're on fire 😄 ! |
Another way to avoid complexity for the user is to simply make multiple
We could even use this to deprecate |
This is something I would like to avoid. Specifically because I think it should be possible for users to write polymorphic functions over the index types. In general with small composable functions you don't care what kind of wrapping mode is chosen as long as the caller ensures the operation is correct for the given index type. |
Wouldn't you just ask for |
Also, I'm not necessarily saying they should be completely different types. They could be aliases of some basic type. |
This implies your functions is intended to be used with all types that implement
Is adding a |
To answer my own question, @leonschoorl offered an example function you might want to write:
Which offers multiple advantages over
I don't know. It's an "add to the to pile" kind of thing. The Clash API is already quite intimidating for new users, especially users not having a Haskell background. Phantom type variables and GADTs don't make it easier. Maybe this is simply solvable by having good documentation. Maybe users don't care as long as there are many examples.
I'd imagine the docs being laid out something like this (don't pin me down on the structure, it's just an example):
where the general case (housed in To be clear, I'm not sold on this proposal either, as it has usability problems too: using type aliases makes GHC prone to "see through" the type in error messages, suddenly confronting users with the general |
The more I think about it, the more I'm inclined to think that my problems aren't actually with your implementation but with:
really should look like
(Look! It's even sorted!) Making superclasses so much more annoying to deal with in documentation.
|
If I look at the haddock output then we can write something behind the instances. But it's really not as nice as having a nice When column which gives the constraint. I will investigate the possibility of a custom type error if the |
@rowanG077 There's a very nice blogpost about custom type errors: https://kodimensional.dev/type-errors |
Something like this is possible for us since there are a finite number of SaturationModes.
|
Yeah, exactly! |
Using this I believe we can get rid of the |
Ignore my last post we can't get rid of it. I'm typing up something right now so we look at it concretely. |
I have updated the code to have a custom type error.
We can work on creating a better message but this is a proof of concept. |
Looks great!
We will :) I've had a quick discussion internally about this PR. We concluded that this is a change we want to make, and that your approach makes sense. We don't want to make it a breaking change for 1.0 however, as we think there's a viable upgrade path:
newtype Index n = [..]
{-# DEPRECATED Index "Use SatIndex instead. Clash 1.2 will change `Index` to be `SatIndex`." #-} (Note that 1.1 will never be released, similar to GHC 8.5)
I'm sorry I'm making you changing it for a second time now 😓 . |
It's fine. This is a draft. The entire point is to discuss the options so we can make a good implementation. How are we going to handle |
375be2d
to
362280b
Compare
648684a
to
762c7d3
Compare
This change should be close to finished now. I have done the following:
|
df0b042
to
71310c7
Compare
@rowanG077 thanks for all the hard work. I should have said this way earlier in the process, but I don't want to change The solution is straightforward, add Then in the documentation, we can steer people towards using |
@christiaanb I don't believe that this will change the API. The newtype wrapper around Maybe an idea to ensure this is the case is to let me finish up this pull request as is and you guys can run this on some larger Clash code bases? |
5d21ca6
to
5f45fb8
Compare
So this PR has gone stale for a while - I think we should either accept it or reject it. In private channels I've heard people are worried it might make Clash code less clear (by default) as wrapping behavior now depends on the type. This would make it less clear in larger code blocks what operations like Given this, I think we should consider two things:
I'm in favor of (1) and against (2). I think overflows should always result in an error unless the user makes an explicit choice (looking at you, Signed / Unsigned / Int). Pinging @christiaanb . |
There's a couple of things:
Ultimately: I don't want any users to have to make any changes to their type signatures when they upgrade to a version of Clash that includes this patch. |
Why the name? |
Well... I already dislike the mistake I made originally calling it Also the name |
I can see that, on its own, the name |
But I'm not suggesting we rename {-# LANGUAGE StandaloneKindSignatures #-}
type BoundedNatural :: SaturationMode -> Nat -> Type
type Index n = BoundedNatural 'SatError n Question: is your issue with using the name
And that calling it |
Right, I like the idea of sharing an implementation. My "complaint" is that there's now one concept (bounded naturals) exported under two different names (
Do these arguments make sense to you?
No, I wouldn't mind. I'm really on the fence on what's better:
I'm very slightly leaning towards the first option because type signatures are already complex enough. To recap, this is what my ideal path would look like:
I don't mind which path is taken, as long as the names are related.
|
Only just looking at this PR now: I have some thoughts on it as a whole. Please don't treat this as me not appreciating your work so far @rowanG077, I'm just wondering if there's a way to implement it and get a more "beginner-friendly" API. The intent of this PR seems to be "for some values you want a different Num instance with a particular behaviour". I think it might instead be worth doing something like: newtype Saturating a where
Saturating :: { getSaturating :: a } -> Saturating a
instance (SaturatingNum a) => Saturating a where
(+) = coerce (satAdd SatBound)
-- other functions similarly using `coerce` for the different default behaviours that may be wanted. From looking at type Wrapping, Saturating, Zeroing :: Type -> Type There doesn't seem a sensible place to put such types right now, I guess they would live under a new The wrapped types would pretty much only have a I'm a bit unsure what the default should be though. I see two convincing options:
This seems a bit better than just adding a Unrelated: In terms of making type Index n = Unsigned (CLog 2 n) If that works it would also mean the special paths for |
@alex-mckenna I really like that idea. Perhaps we could express newtype Saturating a = Saturating a
toSaturating :: a -> Saturating a
toSaturating a = Saturating a
fromSaturating :: Saturating a -> a
fromSaturating (Saturating a) = a
Type synonyms wouldn't work because operations on |
Probably not a good idea, nvm! |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@alex-mckenna I like your approach with one caveat. One of the important things (for me) about this PR in the long run was that it would be possible to phase out the default behaviour of the unsafe
Maybe I'm missing something but creating a |
Just adding this type alone doesn't help so much, no. It makes it better in a way because you can switch any number with a If |
@rowanG077 Great to see you're still active :D. To summarize/reiterate a bit:
This PR used (1) as a vehicle to smoothly introduce (2). Here's my thoughts:
I'm sorry if we're talking in circles a bit @rowanG077. I'm trying to get us to have a definitive answer on the questions you've posed. |
If |
Hmm, not strictly true. But using |
Superseded by #1908 |
Initial draft based on discussion in #680.
There are various issues with this implementation right now.
KnownSatMode
and in some case1 <= n
) are added.fromInteger_INLINE
andfromInteger#
is used. I would like to centralize this overflow/underflow in some function.SaturationMode
type level parameter is confusing for users becauseUnsigned
andSigned
don't have them. Maybe a solution could be to remove theSaturatingNum
instances and only useNum
for numeric types. Or keep theSaturatingNum
instances and use it as a kind of override if you explicitly want different behaviour (this is the route I took in the draft). Either way I think it's best to keep the API for the number types the same.