-
Notifications
You must be signed in to change notification settings - Fork 155
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
base: master
Are you sure you want to change the base?
Add Finite class #2858
Conversation
540afcf
to
4d3695c
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.
Couple of questions! Mostly wondering whether it would make sense to provide an Index
-based Enum
instead.
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' |
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.
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.
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.
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.
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 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]
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.
before/after?
prior/later?
back/forward?
Did not think them through much, just throwing it out there.
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.
fpred
/ fsucc
for finite versions of Enum functions?
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.
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.
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.
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.
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 agree with fpred
/fsucc
.
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 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).
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.
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.
-- [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 @ |
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.
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?
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.
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 ™️.
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 added the demanded tests.
Note that #2871 is required for the tests to run in a reasonable amount of time.
-- 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. |
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.
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.
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 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.
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.
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?
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.
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 ofbase
. - 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 thatInt
~Integer
, i.e., we never enumerate stuff that won't fit into into anInt
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 amaxBound
does not automatically make your type finite, e.g. the interval [0,1] over the reals has aminBound
andmaxBound
too, but is infinite. - The
Bounded
class doesn't support uninhabited types likeVoid
orIndex 0
, which makes it kind of incomplete for me. Though, I think it depends on how you look at it. Nevertheless, having to exclude theIndex 0
case just because of that turns out to be non-elegant in then end.
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.
Gotcha, I think some of this should make it into the documentation. I'm not opposed to "large" classes btw.
7a1399a
to
1130232
Compare
Another thing to consider: there is at least some overlap with Counter I believe. Perhaps |
9564261
to
80dc978
Compare
Good point. Making How about adding another deriving strategy for |
I think however we slice it this PR will be an API change, unless we decide to not export it from
I think |
a0517a0
to
decaec3
Compare
decaec3
to
c8821c4
Compare
index f = index (f . ith <$> indicesI) | ||
|
||
-- | A newtype wrapper, which reverses the index order used by the | ||
-- finite instance of the inner type. |
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.
-- finite instance of the inner type. | |
-- 'Finite' instance of the inner type. |
#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 |
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 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
.
powGTZero :: | ||
forall (n :: Nat) (m :: Nat). | ||
Dict (1 <= n^m) | ||
powGTZero = unsafeCoerce (Dict :: Dict (0 <= 0)) |
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.
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 "*" |
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.
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 |
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 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 Left
s and no Right
s.
{-# OPTIONS_GHC -fplugin GHC.TypeLits.Normalise #-} | ||
{-# OPTIONS_GHC -fplugin GHC.TypeLits.KnownNat.Solver #-} | ||
{-# OPTIONS_GHC -fplugin GHC.TypeLits.Extra.Solver #-} | ||
|
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.
{-# 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.
-- 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 @ |
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 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.
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 toBounded
andEnum
, utilizingIndex
instead ofInt
and vectors instead of lists.Have a look at the haddock documentation for further insights.
Requires
Still TODO: