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

Add Finite class #2858

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open

Add Finite class #2858

wants to merge 2 commits into from

Conversation

kleinreact
Copy link
Member

@kleinreact kleinreact commented Dec 30, 2024

The PR adds the Finite class as well as supplemental instances for most of the standard types.

Finite is a class for types with only finitely many inhabitants and can be considered a more hardware-friendly alternative to Bounded and Enum, utilizing Index instead of Int and vectors instead of lists.

Have a look at the haddock documentation for further insights.

Requires

Still TODO:

  • Write a changelog entry (see changelog/README.md)
  • Check copyright notices are up to date in edited files

Copy link
Member

@martijnbastiaan martijnbastiaan left a comment

Choose a reason for hiding this comment

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

Couple of questions! Mostly wondering whether it would make sense to provide an Index-based Enum instead.

Comment on lines 247 to 251
res = x :> prev'
prev' = case natVal (asNatProxy prev') of
0 -> repeat def
_ -> let next = x +>> prev
in registerB clk rst en (repeat def) next
_ -> let next' = x +>> prev'
in registerB clk rst en (repeat def) next'
Copy link
Member

Choose a reason for hiding this comment

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

The fact that you have to do this is a pretty strong hint these names shouldn't be in Clash.Prelude. Grepping my projects for prev and next also reveals a number of uses.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's kind of unavoidable. Every new name we choose might already be picked up in existing user code and I doubt that it's in the interest of the user, if we choose artificially unintuitive names just because of that.

So yes, the user might experience some new warnings about "clashing" names after a Clash update, which are always easy to fix via renaming or hiding the new stuff from Clash.Prelude, in case they are not desired.

Please note that finding a good (and intuitive) naming scheme that doesn't clash with basic existing libraries (in particular base) is a challenge already, especially if the offered primitives are quire fundamental. I also tried to improve the situation here based on some prior experience I had with the finite library, which comes with a similar interface.

Copy link
Member

@DigitalBrains1 DigitalBrains1 Dec 31, 2024

Choose a reason for hiding this comment

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

I think the point is we write many state machines in Clash and there prev and next are very intuitive local names. Of course you can't avoid all name clashes, but there are some words that are very common in Clash code and prev and next are two examples of that specific category. So I agree with Martijn that trying to come up with an alternative is worthwhile in this specific case.

[edit]
Also, I believe we felt we wanted to move away from primes as a suffix in our code, instead using numeric suffixes to disambiguate. So I believe it'd be better to do that instead of introducing new uses of prime in our code base.
[/edit]

Copy link
Member

Choose a reason for hiding this comment

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

before/after?

prior/later?

back/forward?

Did not think them through much, just throwing it out there.

Copy link
Contributor

Choose a reason for hiding this comment

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

fpred / fsucc for finite versions of Enum functions?

Copy link
Member Author

Choose a reason for hiding this comment

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

succ :: a -> a

Successor of a value. For numeric types, succ adds 1.

pred :: a -> a

Predecessor of a value. For numeric types, pred subtracts 1.

I think that if they wanted to enforce that, then they would have added a law. But yeah, those two sentences are kind of misleading indeed.

Copy link
Member

@DigitalBrains1 DigitalBrains1 Jan 10, 2025

Choose a reason for hiding this comment

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

If the Haddock of a class definition is not normative, what is? They usually use the word law in these cases, but there's no wiggle room in the doc that I see. If they wanted to document something that is common but not normative, I believe they would at the least add the word usually.

[edit]
My point is that these sentences are not misleading. Can you point me to an authoritative text that says otherwise? They are laws. They just didn't use the word law.
[/edit]

[edit 2]
satSucc and satPred are based on Enum. Enum says they should add and subtract one. So the doc for satSucc and satPred says they should add and subtract one.

Originally satSucc and satPred weren't even part of SaturatingNum. But we found that this lead to undesired behaviour of specific instances, so we moved them into the class because they're related enough to justify that.
[/edit 2]

Copy link
Member Author

Choose a reason for hiding this comment

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

My point is that these sentences are not misleading. Can you point me to an authoritative text that says otherwise? They are laws. They just didn't use the word law.

I'm not aware of any authoritative text, but the style of class requirement descriptions in base and ghc-prim varies a lot, so it doesn't look to me like they have a strict enforcement on wording at this point. I gave some examples below. According to those you need to take the Applicative laws very seriously for example, while the equality laws are more kind of a nice-to-have 🙃.

Eq

The Haskell Report defines no laws for Eq. However, == is customarily expected to implement an equivalence relationship where two values comparing equal are indistinguishable by "public" functions, with a "public" function being one not allowing to see implementation details. For example, for a type representing non-normalised natural numbers modulo 100, a "public" function doesn't make the difference between 1 and 201. It is expected to have the following properties:

Ord

The Haskell Report defines no laws for Ord. However, <= is customarily expected to implement a non-strict partial order and have the following properties:

Applicative

Further, any definition must satisfy the following:

Monad

Instances of Monad should satisfy the following:

Copy link
Member Author

@kleinreact kleinreact Jan 10, 2025

Choose a reason for hiding this comment

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

satSucc and satPred are based on Enum.

Sure satSucc has a default implementation, which is satSucc s n = satAdd s n 1, but there is no requirement that I cannot chose a different implementation for my own type instances at this point.

It's also a questionable, whether just because of that every class instance should depend on having a Num instance as well. It would be much more user-friendly to have a default signature that requires Num, so the constraint is only required if the user wants to use the default in the first place, e.g.,

satSucc :: SaturationMode -> a -> a
-- Default method suitable for types that can represent the number 1
default satSucc :: Num a => SaturationMode -> a -> a
satSucc s n = satAdd s n 1
{-# INLINE satSucc #-}

This would elide the requirement on Num for every class instance.

Copy link
Member

Choose a reason for hiding this comment

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

Regarding satSucc and satPred I have primarily not been talking about documentation or implementation, I'm talking about invisible things like design and intent. And also how they changed once we noticed deficiencies.

Sure satSucc has a default implementation, which is satSucc s n = satAdd s n 1, but there is no requirement that I cannot chose a different implementation for my own type instances at this point.

We mean our documentation to be normative. So yes, I definitely interpret it as a requirement that they should add/subtract 1.¹ You yourself argued this above:

Do I miss something fundamental here? I read that like satSucc is equivalent to (+ 1), except for the boundary cases, where the behavior might differ. That's what I consider by "to compatible with each other".

I'd strongly argue that even in the boundary cases the behaviour matches adding/subtracting 1, it's just that (+ 1) does not actually add 1 in those cases.

(continuing with quote from your latest message)

It's also a questionable, whether just because of that every class instance should depend on having a Num instance as well.

I think discussing creating another class for satSucc and satPred is out-of-scope here.

¹ Unless you're talking about boundary cases where the default implementation does not actually satisfy the laws, but then I don't understand why you bring it up.

clash-prelude/src/Clash/Class/Finite/Internal.hs Outdated Show resolved Hide resolved
clash-prelude/src/Clash/Num/Zeroing.hs Show resolved Hide resolved
clash-prelude/src/Clash/Num/Wrapping.hs Show resolved Hide resolved
clash-prelude/src/Clash/Num/Saturating.hs Show resolved Hide resolved
clash-prelude/src/Clash/Class/Finite/Internal.hs Outdated Show resolved Hide resolved
clash-prelude/src/Clash/Class/Finite/Internal.hs Outdated Show resolved Hide resolved
@kleinreact kleinreact force-pushed the finite-class branch 8 times, most recently from 7a1399a to 1130232 Compare January 1, 2025 19:16
@martijnbastiaan
Copy link
Member

Another thing to consider: there is at least some overlap with Counter I believe. Perhaps Counter's superclass should be Finite?

@kleinreact kleinreact force-pushed the finite-class branch 2 times, most recently from 9564261 to 80dc978 Compare January 2, 2025 07:17
@kleinreact
Copy link
Member Author

Another thing to consider: there is at least some overlap with Counter I believe. Perhaps Counter's superclass should be Finite?

Good point. Making Finite a superclass would be a recognizable API change tough. Also, you technically can have a Counter instance for a type with infinitely many inhabitants, so adding the superclass requirement here would introduce a limitation.

How about adding another deriving strategy for Counter via FiniteDerive instead, like already present for Enum? Then the users can decide on their own, whether counting via the implicit index order of the Finite instance is exactly what they want.

@martijnbastiaan
Copy link
Member

Making Finite a superclass would be a recognizable API change tough.

I think however we slice it this PR will be an API change, unless we decide to not export it from Prelude. So that's fine with me (provided that there wouldn't be runtime changes).

How about adding another deriving strategy for Counter via FiniteDerive instead, like already present for Enum? Then the users can decide on their own, whether counting via the implicit index order of the Finite instance is exactly what they want.

I think Finite would count the same as Enum for sum-types right? In that case I don't see the benefit of offering both, but I'm also not opposed to it.

@kleinreact kleinreact force-pushed the finite-class branch 3 times, most recently from decaec3 to c8821c4 Compare January 7, 2025 07:55
@kleinreact kleinreact force-pushed the finite-class branch 3 times, most recently from cb4795a to 8aeab59 Compare January 10, 2025 08:36
@kleinreact kleinreact force-pushed the finite-class branch 3 times, most recently from 645d9fa to 2752731 Compare January 10, 2025 17:01
@DigitalBrains1
Copy link
Member

DigitalBrains1 commented Jan 10, 2025

Could you hide the three internal modules from the Haddock?

Also, I think we should have instances for Clash.Sized.Fixed. I don't know if there are any more instances that were overlooked, but that one occurred to me.

[edit]
That would also give an elegant way to get the next or previous representable value for Fixed. I changed Fixed's Enum instance to be lawful quite some time ago, so it adds or subtracts one on pred or succ. I've come to regret that; we should have documented the deviation, like they subsequently also did with Data.Fixed. Now you have to do stuff like sf . succ . unSF to get the next representable value.
[/edit]

@kleinreact kleinreact force-pushed the finite-class branch 2 times, most recently from ce12c7a to 3719e4a Compare January 10, 2025 19:52
@kleinreact
Copy link
Member Author

kleinreact commented Jan 10, 2025

Could you hide the three internal modules from the Haddock?

👍

[edit]

Also, I think we should have instances for Clash.Sized.Fixed. I don't know if there are any more instances that were overlooked, but that one occurred to me.

I never have used Fixed and don't have much insights into the concept. Maybe we can add that in a later PR.
[/edit]

@DigitalBrains1
Copy link
Member

Okay, when this is merged, could you create an issue that we should add the instance?

Copy link
Member

@DigitalBrains1 DigitalBrains1 left a comment

Choose a reason for hiding this comment

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

I had another pass over the Haddock and feel there's still some needed changes.

'Bounded' instance) such that 'Enum' instances must ship partial
functions for most finite types. Likewise, the number of inhabitants
does not align with the number of indices offered by 'Int' for most
types, which 'Finite' resolves by using 'Index n' instead.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
types, which 'Finite' resolves by using 'Index n' instead.
types, which 'Finite' resolves by using @'Index' n@ instead.

Please check if that fixes the rendering of the Haddock. I think it does, but I'm not sure.

) => Vec (ElementCount a) a
elements = to <$> gElements

-- | The @0@ indexed element.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
-- | The @0@ indexed element.
-- | The element at index 0.

Just a suggestion... it's just that to me it feels a zero-indexed something is something that has indexes starting at 0, like most arrays and stuff in computer science.

) => Maybe a
lowestMaybe = to <$> gLowestMaybe

-- | The @(ElementCount a - 1)@ indexed element
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
-- | The @(ElementCount a - 1)@ indexed element
-- | The element at index @(ElementCount a - 1)@

Copy link
Member

Choose a reason for hiding this comment

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

At 0 I said "just a suggestion" but I changed my mind. I think

The (ElementCount a - 1) indexed element

is an odd sentence, and I would like it changed. That also means it makes complete sense to change the 0-indexed phrasing.

-- | A newtype wrapper that reverses the index order, which normally
-- would be used by the 'Finite' instance of the inner type via
-- 'Generic' deriving. The newtype is only intended be used with the
#if MIN_VERSION_base(4,15,0)
Copy link
Member

Choose a reason for hiding this comment

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

This does not appear to work. CPP and Haddock do not play nice. I think it works better with the {- construction, but still, you'll have to jump through some hoops and you're probably better off with a little duplication. Otherwise you'll get CPP-generated markers as part of your Haddock.

Please test with GCC versions both matching the #if and the #else.

Note how your documentation ends at "used with the" in the Haddock generated in CI:
https://clash-lang.gitlab.io/-/clash-compiler/-/jobs/8839034866/artifacts/hadocs/clash-prelude/clash-prelude/Clash-Class-Finite.html#t:GenericReverse

Comment on lines +33 to +35
-- * Extensions
, GenericReverse(..)
, WithUndefined(..)
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this also list BoundedEnumEq? Right now the only mention of that is at the

(Bounded a, Enum a, Eq a, KnownNat n, 1 <= n) => Finite (BoundedEnumEq n a)

instance, and if you then click

see BoundedEnumEq

you're taken to the Internal module. I don't think we should ever say "see the internal module" in our doc.

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.

5 participants