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

Choose a reason for hiding this comment

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

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.

IMO this again reinforces my point: base's Prelude (+Haskell itself) already squats an uncomfortable number of identifiers. Another point is that I don't really like synonyms. I can basically never remember whether to use min or minimum for example, I'd have the same problems with succ and next. (Though TBF I've used succ so many times now that I'd recognize next as the newer identifier, but I think my point is clear.)

fpred / fsucc makes sense to me.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the feedback and all the suggestions. I like to change it to nextElement and previousElement then, which I still consider intuitive and recognizable.

While fpred and fsucc are nice and short, they still might be mixed up with their Enum counterparts, which I'd prefer to avoid.

Copy link
Member

Choose a reason for hiding this comment

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

I agree with fpred/fsucc.

Copy link
Member Author

@kleinreact kleinreact Jan 1, 2025

Choose a reason for hiding this comment

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

I changed the function names to before and after now, as I just didn't feel comfortable with the options discussed so far (except before and after, as suggested by @DigitalBrains1).

Copy link
Member

@martijnbastiaan martijnbastiaan Jan 1, 2025

Choose a reason for hiding this comment

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

Thanks for the feedback and all the suggestions. I like to change it to nextElement and previousElement then, which I still consider intuitive and recognizable.

This suffers from the same synonym (min vs minimum) issue I mentioned.

Edit: didn't see your message after. Though before and after still do. I don't get what's so bad about fpred/fsucc? At least 3 people in this thread seem to be fine with it.

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
Comment on lines 125 to 138
-- [Index Order]
-- @ index '<$>' elements = 'indicesI' @
-- [Forward Iterate]
-- @ 'iterateI' ('>>=' next) (lowest \@a) = 'Just' '<$>' (elements \@a) @
-- [Backward Iterate]
-- @ 'iterateI' ('>>=' prev) (highest \@a) = 'Just' '<$>' (elements \@a) @
-- [Index Isomorphism]
-- @ith (index x) = x@
-- [Minimum Predecessor]
-- @ lowest '>>=' prev = 'Nothing' @
-- [Maximum Successor]
-- @ highest '>>=' next = 'Nothing' @
-- [No Uninhabited Extremes]
-- @ lowest \@a = 'Nothing' /and/ highest \@a = 'Nothing'
-- /if and only if/ ElementCount a = 0 @
Copy link
Member

Choose a reason for hiding this comment

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

Would it be sensible to add hedgehog laws for this? Could these be added to the test suite for the types that derive / define an instance?

Copy link
Member Author

Choose a reason for hiding this comment

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

Definitely! It's just not necessary at the current code reviewing stage IMO. Hence, I considered it to be a nice addition for a later stage. I also tested all of the features in REPL already ™️.

Copy link
Member Author

Choose a reason for hiding this comment

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

I added the demanded tests.

Note that #2871 is required for the tests to run in a reasonable amount of time.

Comment on lines 95 to 93
-- In general, the class can be considered as a more hardware-friendly
-- alternative to 'Bounded' and 'Enum', utilizing 'Index' instead of
-- 'Int' and vectors instead of lists.
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason that we don't provide FiniteEnum (or CEnum) directly? This would give developers an immediate intuition for what the classes are.

Related: I've wondered many times whether it would be sensible for Clash.Prelude to export a Clash-friendly version of all these Int/Integer based APIs. I.e., replace base's Enum (and friends) in favor of Clash ones.

Copy link
Member Author

Choose a reason for hiding this comment

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

I would not change the semantics of the classes that are already available in base, because this only leads to trouble and confusion. The right strategy should be to provide hardware-friendy alternatives, like Signed n is to be preferred over Int in hardware, which then can coexist with the software-friendly types.

Most of the design choices for this extension have been inspired by my finite library for sure, where I considered it more useful to have a single class covering all of the properties of a "finite" type, instead of breaking things up into multiple classes like Enum or Bounded, which definitely makes sense for types with infinitely many inhabitants.

I'd also like to avoid to make the interface here too similar to Enum and friends, because there still are some fundamental differences, which could be mixed up easily otherwise.

Copy link
Member

Choose a reason for hiding this comment

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

Most of the design choices for this extension have been inspired by my finite library for sure, where I considered it more useful to have a single class covering all of the properties of a "finite" type, instead of breaking things up into multiple classes like Enum or Bounded, which definitely makes sense for types with infinitely many inhabitants.

I'd also like to avoid to make the interface here too similar to Enum and friends, because there still are some fundamental differences, which could be mixed up easily otherwise.

Could you please elaborate? Both Enum and Bounded are mentioned in the documentation, but their relation (or rather differences) aren't clear to me. What makes them good classes for "infinite" types, but not for "finite" types?

Copy link
Member Author

Choose a reason for hiding this comment

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

Enum

  • Already the type of succ, pred :: Enum a => a -> a assumes that every element has successor and predecessor, which makes perfect sense for infinite types, but requires finite types to error on certain inputs. Note that wrapping behavior is disallowed according to the documentation of base.
  • As a consequence, Enum instances must ship partial functions for most finite types.
  • The same way, using Int as the index type creates a natural mismatch between the number of inhabitants of the index type with the ones of the indexed type. For infinite types, that's "kind of ok", because here the practical assumption is that Int ~ Integer, i.e., we never enumerate stuff that won't fit into into an Int anyway, but that's just an efficiency trade off in the end.
  • Generic deriving for Enum is only supported for sum data types, but they cannot contain any products (because that doesn't work out with infinite types), which is kind of a strange conceptual mix between the two worlds.

Bounded

  • The class is often used to identify the bounds of finite types, but just having a minBound and a maxBound does not automatically make your type finite, e.g. the interval [0,1] over the reals has a minBound and maxBound too, but is infinite.
  • The Bounded class doesn't support uninhabited types like Void or Index 0, which makes it kind of incomplete for me. Though, I think it depends on how you look at it. Nevertheless, having to exclude the Index 0 case just because of that turns out to be non-elegant in then end.

Copy link
Member

Choose a reason for hiding this comment

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

Gotcha, I think some of this should make it into the documentation. I'm not opposed to "large" classes btw.

@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 2 times, most recently from a0517a0 to decaec3 Compare January 6, 2025 09:23
index f = index (f . ith <$> indicesI)

-- | A newtype wrapper, which reverses the index order used by the
-- finite instance of the inner type.
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
-- finite instance of the inner type.
-- 'Finite' instance of the inner type.

Comment on lines +26 to +44
#if MIN_VERSION_base(4,16,0)
DecsQ
deriveFiniteTuples finiteName = do
let finite = ConT finiteName
allNames <- replicateM maxTupleSize $ newName "a"

forM [3..maxTupleSize] $ \tupleNum -> do
let names = take tupleNum allNames
vs = case map VarT names of
(z:zs) -> (z:zs)
_ -> error "maxTupleSize < 3"
tuple xs = foldl' AppT (TupleT $ length xs) xs

-- Instance declaration
context = AppT finite . VarT <$> names
instTy = AppT finite $ tuple vs

return $ InstanceD Nothing context instTy []
#else
Copy link
Member

Choose a reason for hiding this comment

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

I was confused why there was this difference between version.
And why there was a difference for the 2-tuple instance.

I guess for the older base you ran into problems that the Generic instances only go up to 7-tuples.
But even for the latest base Generic instances only seem to be defined up to 15-tuples.
So I think that means we need the more complex version even on newer version if maxTuplesSize is increased past 15 by enabling large-tuples.

Comment on lines +26 to +29
powGTZero ::
forall (n :: Nat) (m :: Nat).
Dict (1 <= n^m)
powGTZero = unsafeCoerce (Dict :: Dict (0 <= 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 isn't true when n = 0:
0^m = 0 (for every m /= 0)

highestName beforeName afterName ithName indexName = do
let finite = ConT finiteName
elementCount = ConT elementCountName
times = ConT $ mkName "*"
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
times = ConT $ mkName "*"
times = ConT ''(*)

Along with import GHC.TypeNats (type (*))

elements = to <$> gElements

-- | Just the @0@ indexed element. Nothing if @ElementCount a = 0@.
lowest :: Maybe a
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit conflicted about the Maybe on lowest and highest.
As in practice it seems a bit annoying.
And I'm unsure how useful Finite instances for empty types are.
But it is cool that elements @(Either (Maybe Bool) Void) is just a vector with only Lefts and no Rights.

{-# OPTIONS_GHC -fplugin GHC.TypeLits.Normalise #-}
{-# OPTIONS_GHC -fplugin GHC.TypeLits.KnownNat.Solver #-}
{-# OPTIONS_GHC -fplugin GHC.TypeLits.Extra.Solver #-}

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
{-# OPTIONS_HADDOCK not-home #-}

Adding this should result in haddock links going to Clash.Class.Finite instead, where you have the nice extra module documentation.

Comment on lines +120 to +138
-- Any definition must satisfy the following laws (automatically
-- ensured when generic deriving the instance):
--
-- [Index Order]
-- @ index '<$>' elements = 'indicesI' @
-- [Forward Iterate]
-- @ 'iterateI' ('>>=' after) (lowest \@a) = 'Just' '<$>' (elements \@a) @
-- [Backward Iterate]
-- @ 'iterateI' ('>>=' before) (highest \@a)
-- = 'Just' '<$>' 'reverse' (elements \@a) @
-- [Index Isomorphism]
-- @ith (index x) = x@
-- [Minimum Predecessor]
-- @ lowest '>>=' before = 'Nothing' @
-- [Maximum Successor]
-- @ highest '>>=' after = 'Nothing' @
-- [No Uninhabited Extremes]
-- @ lowest \@a = 'Nothing' /and/ highest \@a = 'Nothing'
-- /if and only if/ ElementCount a = 0 @
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it makes sense to add something like:

-- If this type is also a `Bounded` instance, it should satisfy:
-- @ lowest = Just minBound @
-- @ highest = Just maxBound @

This would make the current instance of Down unlawful.

But maybe that's a good thing.
The whole thing of Down is that it flips things like Ord,Bounded,Enum, so it probably makes sense that it flips the Finite ordering too.
You could derive the Down instance via ReversedIndexOrder, or alternatively drop ReversedIndexOrder and rely on Down instead.

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