-
Notifications
You must be signed in to change notification settings - Fork 48
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
WIP: Revive compact representation for NP/NS #129
base: master
Are you sure you want to change the base?
Conversation
107facc
to
cd9f2ef
Compare
@@ -1,5 +1,5 @@ | |||
name: sop-core | |||
version: 0.5.0.1 | |||
version: 0.5.1.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.
FWIW, this is definitely not a non-breaking change.
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.
OTOH, staged-streams
compiles fine. (Plenty of warnings about non-exhautive pattern matches on NS
, but those are not fatal).
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.
Yeah, my hope was that with the pattern synonym it would be backwards compatible; and those warnings should even be absent from 8.10.
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 data structure is now stricter (spine strict?). This is contrived example, but still, the behavior changes observably:
Currently,
hd $ I 'x' :* (error "ay, caramba!" :: NP I '[])
I 'x'
With this patch
*Data.SOP Data.SOP.NP> hd $ I 'x' :* (error "ay, caramba!" :: NP I '[])
I *** Exception: ay, caramba!
CallStack (from HasCallStack):
error, called at <interactive>:21:16 in interactive:Ghci1
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.
Ah, fair enough. Hadn't thought terribly hard about strictness just yet :)
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 good reason why someone might rely on the construction of the NP being lazy? the idea of writing code which might only look at the first N elements of a NP seems pretty odd 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.
FWIW, I think that NP
and NS
should be spine-strict, and not making them so from the beginning was a mistake. Before sop-core
, I'd have said there's basically no reason not to make the change. Now, it's theoretically possible there are uses that aren't generic programming related which might have other requirements. But it may be worth just doing it.
deriving instance All (Eq `Compose` f) xs => Eq (NS f xs) | ||
deriving instance (All (Eq `Compose` f) xs, All (Ord `Compose` f) xs) => Ord (NS f xs) | ||
instance All (Show `Compose` f) xs => Show (NS f xs) where | ||
show ns @ (NS i _) = |
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.
Add TODO to implement this properly. To be backward compatible this needs to print S
s and Z
s.
BTW. The space between @
and (NS ...
will break on GHC-9.0 due ghc-proposals/ghc-proposals#229
src/Data/SOP/NS.hs:197:3: error:
Parse error in pattern: show
In a function binding for the ‘@’ operator.
Perhaps you meant an as-pattern, which must not be surrounded by whitespace
|
197 | show ns @ (NS i _) =
(Otherwise this patch is GHC-9.0 friendly)
I just came to repo to see if it would be possible to have a more compact representation of data NS (f :: k -> Type) (ts :: [k]) = NS Word Any I’m less sure if it would have as much of a performance improvement as the rest of this PR, but it would definitely improve memory usage. |
Yes, that's certainly possible. The original branch from which I was working does in fact do that. For my current purposes however, it turns out this representation is still not quite good enough, so I'm not sure if this work will go anywhere right now. |
Ah great, I found the branch and it looks like it was a little more complicated than I expected (I had started playing with an implementation of the idea this afternoon but ran into some of the issues that the original branch ran into, such a making instances for Show, Eq etc. work) I would be really keen to see some benchmarks before and after with the original, your PR's work, the compact NS implementation, and both (though I understand if you aren't keen do the last two). |
Perhaps @kosmikus did some on the original compact representation branch? |
I did only do runtime benchmarking, and I wasn't too impressed. I think staging is the far superior approach for runtime performance. Compile-time is a different question though. If the goal is that any compact-representation branch would become the default at any point, it needs thorough benchmarking though. I don't want to replace a safe implementation with a potentially unsafe one for questionable / not noticeable gain. |
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 basically convinced we should finally do this. There's still a lingering question whether we should also offer the old inductive versions in sop-core
. Advantages could be:
- somewhat easier to do performance comparisons if both are available in a single package
- other use cases of
sop-core
might need to use e.g.:*
for construction in a now less efficient way
My remarks are basically points for discussion. I'm happy to make the changes myself, once we reach consensus.
The more important point is that this is only partial though, and we need to make changes to the other functions before merging this. While it should all be correct as it currently is, because the old constructors are faithfully reimplemented, many functions can and should be much more efficiently implemented in terms of the underlying compact representation.
data NP :: (k -> Type) -> [k] -> Type where | ||
Nil :: NP f '[] | ||
(:*) :: f x -> NP f xs -> NP f (x ': xs) | ||
newtype NP (f :: k -> *) (xs :: [k]) = NP (V.Vector Any) |
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 still unconvinced that Any
is better than f Any
here. Do you have any data points on this?
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 started out with just Any
on my branch, despite having seen you use f Any
, because I didn't see the point. Then later on I realized that actually having that f
there is really rather helpful; often that f
is concrete (say, an applicative functor or something) and having, say, f (Any -> Any)
and f Any
in hand is a lot more helpful than Any
and Any
.
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.
(Well, maybe not concrete concrete, but at least we know something about the structure.)
infixr 5 :* | ||
|
||
#if __GLASGOW_HASKELL__ >= 802 | ||
{-# COMPLETE Nil, (:*) #-} | ||
#endif |
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.
We don't need this CPP anymore, as 8.0 is the oldest GHC version we support even now, but in practice, we're probably going to shift the lower bound to 8.6 soon.
-- | n-ary products (and products of products) | ||
module Data.SOP.NP | ||
( -- * Datatypes | ||
NP(..) | ||
NP(.., Nil, (:*)) |
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.
Do we want to export the NP
constructor, or do we want to move it to an Internal
module?
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.
Moving it would make sense to me, otherwise there are bound to be uses of NP
that use the patterns dictionary and they would now suddenly get much worse efficiency.
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 definitely want to be able to access the NP constructor as a developer, but putting it in a .Internal
modules makes sense as it doesn't provide a safe interface, which Nil
and (:*)
do. If I know what I'm doing and performance matters to me, I'd want to be able to work with the Vector directly and convince myself I did so safely.
=> forall x xs . (xs' ~ (x ': xs)) => f x -> NP f xs -> NP f xs' | ||
pattern x :* xs <- (viewNP -> IsCons x xs) | ||
where | ||
x :* NP xs = NP (V.cons (unsafeCoerce x) xs) |
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 should be a documentation warning here that the complexity of this for construction is O(n).
data NS :: (k -> Type) -> [k] -> Type where | ||
Z :: f x -> NS f (x ': xs) | ||
S :: NS f xs -> NS f (x ': xs) | ||
data NS (f :: k -> *) (xs :: [k]) = NS !Int Any |
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.
Same remark as before. Why not f Any
?
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.
We probably should add an UNPACK
pragma for good measure.
-- | n-ary sums (and sums of products) | ||
module Data.SOP.NS | ||
( -- * Datatypes | ||
NS(..) | ||
NS(.., Z, S) |
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.
Same remark as for NP
regarding the export of the NS
constructor.
go (fs :* _ ) (Z xs ) = Z (ap_NP fs xs ) | ||
go (_ :* fss) (S xss) = S (go fss xss) | ||
go Nil x = case x of {} | ||
go (NP nps) (NS i ns) = NS i (unsafeCoerce ap_NS (nps V.! i) ns) |
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.
Why not inline go
here?
FWIW, I've started working on porting the rest of the old compact-representation branch. |
Is there any word on progress of this PR? |
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 noticed a missing optimisation based on the new representation. Is there any chance this PR will actually get merged? It's been stagnant for a few months now.
@@ -201,8 +278,7 @@ shiftEjection (Fn f) = Fn $ (\ns -> case ns of Z _ -> Comp Nothing; S s -> f (K | |||
-- @since 0.2.2.0 | |||
-- | |||
unZ :: NS f '[x] -> f x | |||
unZ (Z x) = x | |||
unZ (S x) = case x of {} | |||
unZ (NS _ x) = unsafeCoerce x | |||
|
|||
-- | Obtain the index from an n-ary sum. | |||
-- |
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.
Commenting here for the index_NS
definition below, but this PR should also have:
index_NS :: forall f xs . NS f xs -> Int
index_NS (NS i _) = i
This is an attempt to revive https://github.com/well-typed/generics-sop/tree/compact-representation .