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

WIP: Revive compact representation for NP/NS #129

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

edsko
Copy link

@edsko edsko commented Jan 14, 2021

@edsko edsko force-pushed the edsko/compact-representation branch from 107facc to cd9f2ef Compare January 14, 2021 09:29
@@ -1,5 +1,5 @@
name: sop-core
version: 0.5.0.1
version: 0.5.1.0
Copy link
Contributor

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.

Copy link
Contributor

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).

Copy link
Author

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.

Copy link
Contributor

@phadej phadej Jan 14, 2021

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

Copy link
Author

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 :)

Copy link

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.

Copy link
Member

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 _) =
Copy link
Contributor

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 Ss and Zs.

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)

@axman6
Copy link

axman6 commented Jan 18, 2021

I just came to repo to see if it would be possible to have a more compact representation of NS, could a similar thing be done using

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.

@edsko
Copy link
Author

edsko commented Jan 19, 2021

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.

@axman6
Copy link

axman6 commented Jan 19, 2021

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).

@edsko
Copy link
Author

edsko commented Jan 19, 2021

Perhaps @kosmikus did some on the original compact representation branch?

@kosmikus
Copy link
Member

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.

Copy link
Member

@kosmikus kosmikus left a 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)
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 still unconvinced that Any is better than f Any here. Do you have any data points on this?

Copy link
Author

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.

Copy link
Author

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
Copy link
Member

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, (:*))
Copy link
Member

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?

Copy link
Author

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.

Copy link

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)
Copy link
Member

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
Copy link
Member

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?

Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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?

@kosmikus
Copy link
Member

FWIW, I've started working on porting the rest of the old compact-representation branch.

@ghost
Copy link

ghost commented Oct 27, 2021

Is there any word on progress of this PR?

Copy link

@ghost ghost left a 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.
--
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants