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

Use a more idiomatic edgedb-native Range #341

Open
CodesInChaos opened this issue Aug 28, 2024 · 5 comments
Open

Use a more idiomatic edgedb-native Range #341

CodesInChaos opened this issue Aug 28, 2024 · 5 comments

Comments

@CodesInChaos
Copy link
Contributor

CodesInChaos commented Aug 28, 2024

The native EdgeDB type looks like this:

pub struct Range<T> {
    pub(crate) lower: Option<T>,
    pub(crate) upper: Option<T>,
    pub(crate) inc_lower: bool,
    pub(crate) inc_upper: bool,
    pub(crate) empty: bool,
}

I think a more idomatic way of representing that would be one of the following. All of these can still represent every valid edgedb value.

I don't think these are breaking changes, since the current struct has private fields. However the fields have corresponding public accessors, which I'd like to get rid of if breaking changes are allowed (or at least deprecate).

use core::ops::Bound;

pub enum Range<T> {
    Empty,
    NonEmpty(Bound<T>, Bound<T>),
}

or

use core::ops::Bound;

pub enum Range<T> {
    Empty,
    NonEmpty { lower: Bound<T>, upper: Bound<T> },
}

or

use core::ops::Bound;

pub struct BoundedRange<T> {
    lower_bound: Bound<T>,
    upper_bound: Bound<T>,
}

pub enum Range<T> {
    Empty,
    NonEmpty(BoundedRange<T>),
}

or

pub struct BoundedRange<T>; // as above

pub struct MaybeEmptyRange<R>(inner:Option<R>);

type Range<T> = MaybeEmptyRange<BoundedRange<T>>;

I like the last one best, since it NonEmptyRange could be useful to be used in the model if the user knows it's never empty. It could also come in handy to model multi-ranges as something like Vec<NonEmptyRange<T>> (I'm assuming multi-ranges can't contain individual empty ranges).

@aljazerzen
Copy link
Contributor

aljazerzen commented Aug 28, 2024

I tend to be against breaking changes whose justification is "it makes more sense some other way". Fortunately for this issue, there is a good reason for the proposed change:

Current range definition allows the following to exist:

Range {
    lower: None,
    upper: None,
    inc_lower: true,
    inc_upper: true,
    empty: false,
}

... which is very strange, as not having a bound and it being an inclusive does not go together. It is also possible to set empty to true and set other properties, which is also confusing.


I like your option 3 the most. Feels most understandable to me, it does not use type aliases and it also support the "modelling when user knows the range not to be empty".

BoundedRange<T> could also be reused when implementing multi-ranges:

struct MultiRange<T>(Vec<BoundedRange<T>>)

@CodesInChaos
Copy link
Contributor Author

CodesInChaos commented Aug 28, 2024

Option 1 to 3 have two problems that option 4 can fix:

  • non canonical empty ranges, like an exclusive range where start == end, or an inclusive range where start > end. These could be represented as NonEmpty(actually-empty)
  • When using the built in rust Range/RangeInclusive it will not be possible to deserialize empty ranges, since they only represent empty ranges via start > end/end >= start respectively. So a generic MaybeEmptyRange wrapper could be used with them as well.

The basic changes above aren't breaking changes by themselves. But removing the weird accessors is.

And adding a PartialOrd constraint on the contained value to test for emptiness is breaking too, though all the types supported by edgedb ranges implement that trait anyways.


I think for now I'll go for a mix of 3 and 4:

pub struct BoundedRange<T>; // as above

pub struct Range<T>(Option<BoundedRange<T>>);

impl<T> Range<T> {
    fn is_empty(&self) -> bool;
    fn non_empty(&self) -> &BoundedRange<T>;
}

This allows fixing the non-canonical empty problem, and defers handling of std::ops. Deferring might be a good idea for now, especially since Rust 2024 will bring some changes in that area.

@CodesInChaos
Copy link
Contributor Author

Another problematic feature is: #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] on the existing struct. This leaks the format of the private fields into json. I would suggest adding manual implementations of these, and make those equivalent to how <json>range(...) works in EdgeQL.

@CodesInChaos
Copy link
Contributor Author

CodesInChaos commented Aug 28, 2024

If we use PartialOrd to limit BoundedRange to non-empty ranges, then making Range an enum works. That seems like the cleanest design, even if means that constructing a BoundedRange from std::ops ranges would be fallible.


edit: Looks like causes problems forRange<Box<Value>>, but they can probably be worked around

@CodesInChaos
Copy link
Contributor Author

CodesInChaos commented Aug 28, 2024

Unfortunately there were some unexpected challenges:

NaN handling

Edgedb treats NaN as larger than any normal value and equal to itself, while rust treats it as incomparable. I solved this by introducing a private EdgedbOrd trait which follows EdgeDB's comparison rules.

I also implemented that trait for Value, and panic if the compared values don't have the same type. I don't think it's actually possible to hit the panic, since constructing a Value::Range starts with a strongly typed Range. This might need adjustments once I implement deserialization.

Canonicalizing Bounds

EdgeDB cononicalizes discrete ranges so the start is inclusive and the end exclusive. The constructors of the rust range types do the same. This is done via the RangeScalar trait, which serves both as marker for the traits EdgeDB ranges support, and to enable this functionality.

See PR #345 (doesn't compile yet)

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

No branches or pull requests

2 participants