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

TreapMap.keys as TreapSet #20

Merged
merged 6 commits into from
Jan 7, 2025
Merged

TreapMap.keys as TreapSet #20

merged 6 commits into from
Jan 7, 2025

Conversation

ericeil
Copy link
Collaborator

@ericeil ericeil commented Dec 21, 2024

Currently it is quite expensive to do operations like this:

set2 = map1.keys intersect set1

This is because the keys property on TreapMap is an ImmuableSet, not a TreapSet, so the intersection operation must convert it to a TreapSet first, using the generic Set->TreapSet conversion, which is rather expensive.

This is a shame, because a TreapMap<K, V> of course is already structured the same way as a TreapSet<K>, so the conversion to TreapSet should be very cheap.

This PR redefines the keys property as a TreapSet, and defines efficient implementations of this. Depending on usage, we can sometimes avoid the conversion to TreapSet altogether; if not, we can do the conversion as a straightforward projection of the existing TreapMap structure, avoiding most of the overhead of the generic conversion.

We also add single and singleOrNull methods to TreapMap, which are useful in their own right, and can be used to avoid the conversion to TreapSet in some cases.

Currently it is quite expensive to do operations like this:

```
set2 = map1.keys intersect set1
```

This is because the `keys` property on `TreapMap` is an `ImmuableSet`, not a `TreapSet`, so the intersection operation must convert it to a `TreapSet` first, using the generic `Set`->`TreapSet` conversion, which is rather expensive.

This is a shame, because a `TreapMap<K, V>` of course is already structured the same way as a `TreapSet<K>`, so the conversion to `TreapSet` should be very cheap.

This PR redefines the `keys` property as a `TreapSet`, and defines efficient implementations of this.  Depending on usage, we may be able to avoid the conversion to `TreapSet` altogether; if not, we can do the conversion as a straightforward projection of the existing `TreapMap` structure, avoiding most of the overhead of the generic conversion.

We also add `single` and `singleOrNull` methods to `TreapMap`, which are useful in their own right, and can be used to avoid the conversion to `TreapSet` in some cases.
@ericeil ericeil changed the title TreapMap.keys as TreapSet TreapMap.keys as TreapSet Jan 2, 2025
@ericeil ericeil marked this pull request as ready for review January 2, 2025 20:02
@ericeil ericeil requested review from jtoman and yoav-rodeh January 2, 2025 20:03
Copy link

@yoav-rodeh yoav-rodeh left a comment

Choose a reason for hiding this comment

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

some questions

override fun shallowGetSingleElement(): E = treapKey
override fun singleOrNull(): E? = treapKey.takeIf { left == null && right == null }
override fun single(): E = treapKey.also {
if (left != null || right != null) { throw IllegalArgumentException("Set contains more than one element") }

Choose a reason for hiding this comment

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

any reason not to do (here and also in the hash version)

    override fun single(): E = singleOrNull() ?: throw ...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

E might be nullable, so singleOrNull doesn't give the correct behavior.

override fun singleOrNull() = map.singleOrNull()?.key
override fun arbitraryOrNull() = map.arbitraryOrNull()?.key

override fun containsAny(elements: Iterable<K>) = keys.value.containsAny(elements)

Choose a reason for hiding this comment

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

why do you create the keys set for containsAny and containsAll?
is it because if elements happens to be a TreapSet it's more efficient?

but then if elements is not then then can be pretty costly. I'm especially thinking of the case where keys is very large and elements is tiny. You'll run in time O(|keys|).

maybe you can check here if elements is a TreapSet or not, and according to that choose what to do?

Copy link
Collaborator Author

@ericeil ericeil Jan 7, 2025

Choose a reason for hiding this comment

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

Yes, I'm opting for simplicity here. The problem you point out (large map, small elements) is still a problem even if everything's a treap. Optimizing that is tricky, since getting the size of a Treap (as you point out elsewhere) is surprisingly expensive.

We could have different cases here:

  • Empty elements
  • Single Treap element
  • Non-treap (maybe broken down further by size?)

...but I'd much rather just keep this simple for now. It's possible we will never even use this method. :)

Choose a reason for hiding this comment

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

alright, I agree it's likely that calling these will be pretty rare.

Presents the keys of a [TreapMap] as a [TreapSet].

The idea here is that a `TreapMap<K, *>` is stored with the same Treap structure as a `TreapSet<K>`, so we can very
quickly create the corresponding `TreapSet<K>` when needed, in O(1) time.

Choose a reason for hiding this comment

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

I think it's O(n) time, instead of the naive algorithm to create the set, which would take O(n log(n)), no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Haha yes, this was really wishful thinking. :)

(At one point I thought I could do this in O(1) time, by having the maps actually implement TreapSet, but that idea didn't pan out)

override fun toString() = keys.value.toString()

override val size get() = map.size
override fun isEmpty() = map.isEmpty()

Choose a reason for hiding this comment

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

i always find it very unintuitive that size for treaps is an expensive operation. No avoiding it really I guess...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, we could store the size in the treap nodes, but of course that increases memory usage. So far this still seems like the right tradeoff.

@@ -27,6 +27,7 @@ internal class SortedTreapSet<@Treapable E>(
override fun Iterable<E>.toTreapSetOrNull(): SortedTreapSet<E>? =
(this as? SortedTreapSet<E>)
?: (this as? PersistentSet.Builder<E>)?.build() as? SortedTreapSet<E>

Choose a reason for hiding this comment

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

a question about line 29, how come you check if it is a PresistentSet.Builder and not SortedTreapSet.Builder?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There is no SortedTreapSet.Builder.

private fun treapSetFromKeys(): SortedTreapSet<K> =
SortedTreapSet(treapKey, left?.treapSetFromKeys(), right?.treapSetFromKeys())

@Suppress("Treapability")

Choose a reason for hiding this comment

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

why do you suppress here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The treapability analysis doesn't see that the superclass's hashCode implementation is stable. I'll fix this a different way.


@Suppress("Treapability")
class KeySet<@Treapable K>(
override val map: SortedTreapMap<K, *>,

Choose a reason for hiding this comment

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

is there no avoiding the * here?
can't KeySet be an inner class, and then you get K and V for free?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It didn't occur to me to make this an inner class! That's a good idea (though you'll see the * pop up elsewhere after this change :) )

@ericeil ericeil requested a review from yoav-rodeh January 7, 2025 16:18
Comment on lines 233 to 235
override fun single(): E = element.also {
if (next != null || left != null || right != null) { throw IllegalArgumentException("Set contains more than one element") }
}
Copy link

Choose a reason for hiding this comment

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

just because you can write something as a one liner, doesn't mean you should.

This "post fixing" conditional stuff sucks in ruby, let's not bring it into kotlin :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fine. :)

override fun shallowGetSingleElement(): E = treapKey
override fun singleOrNull(): E? = treapKey.takeIf { left == null && right == null }
override fun single(): E = treapKey.also {
if (left != null || right != null) { throw IllegalArgumentException("Set contains more than one element") }
Copy link

Choose a reason for hiding this comment

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

same objection as above. Just use a block body

@@ -31,6 +31,7 @@ internal class HashTreapMap<@Treapable K, V>(
this as? HashTreapMap<K, V>
?: (this as? PersistentMap.Builder<K, V>)?.build() as? HashTreapMap<K, V>

override fun singleOrNull() = MapEntry(key, value).takeIf { next == null && left == null && right == null }
Copy link

Choose a reason for hiding this comment

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

you might wonder why I don't object to this, where I am objecting to the also nonsense. I confess I don't have a great rule of thumb, except this looks more idiomatic, and the takeIf is clearly indicating what's happening, whereas using also to exceptionally abort is pretty surprising

@ericeil ericeil requested a review from jtoman January 7, 2025 19:25
Copy link

@yoav-rodeh yoav-rodeh left a comment

Choose a reason for hiding this comment

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

lgtm!

@ericeil ericeil enabled auto-merge (squash) January 7, 2025 20:48
@ericeil ericeil merged commit e2d8c8e into Certora:main Jan 7, 2025
1 check passed
@ericeil ericeil deleted the mapKeys branch January 7, 2025 20:49
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.

3 participants