Skip to content

Implement segtree #37

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

Merged
merged 23 commits into from
Sep 17, 2020
Merged

Conversation

TonalidadeHidrica
Copy link
Collaborator

No description provided.

@TonalidadeHidrica
Copy link
Collaborator Author

TonalidadeHidrica commented Sep 11, 2020

Basic implementation has ended, so I'm opening this PR. The following is my additional TODO:

  • Write common monoid traits (Max, Min, Sum, and even more?)
  • Write sample code that passes J - Segment Tree
  • Write some tests

@TonalidadeHidrica TonalidadeHidrica marked this pull request as ready for review September 11, 2020 15:30
@qryxip
Copy link
Member

qryxip commented Sep 11, 2020

We may use this as a reference

alga::general

though with current implementation we provide common Monoid such as Additive in this way:

use std::{convert::Infallible, marker::PhantomData, ops::Add};

enum Additive<S> {
    __Void(Infallible, PhantomData<fn() -> S>),
}

impl<S: ConstZero> Monoid for Additive<S> {
    type S = S;

    const IDENTITY: S = S::ZERO;

    #[inline]
    fn binary_operation(a: S, b: S) -> S {
        a + b
    }
}

trait ConstZero: Sized + Copy + Add<Output = Self> {
    const ZERO: Self;
}

macro_rules! impl_const_zero {
    ($($ty:ty),*) => {
        $(
            impl ConstZero for $ty {
                const ZERO: Self = 0;
            }
        )*
    }
}

impl_const_zero!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize);

src/segtree.rs Outdated

pub trait Monoid {
type S: Copy;
Copy link
Member

Choose a reason for hiding this comment

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

We will possibly want to use non Copy types such as matrix for S.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Then should we use Clone instead?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that is what I meant. identities and binary_operations will be inlined.

@TonalidadeHidrica
Copy link
Collaborator Author

@qryxip About monoid trait, that's what I was going to do. Is there any difference between my current implementation and alga?

@TonalidadeHidrica
Copy link
Collaborator Author

TonalidadeHidrica commented Sep 11, 2020

I'm now wondering how can I declare the identity of Max: Monoid trait. I tried to use Integral::max_value but I couldn't, because we can only use constant functions for associated constants, and trait functions cannot be declared as const. Any idea?

I have currently two ideas:

  • Use fn identity() -> Self::S instead of const IDENTITY: Self::S. This may cause slight performance degression, but becomes more flexible and easier to implement.
  • Add associated constants in Integral (and similar) traits. We can keep IDENTITY constant, but I don't know how can it achieved.

@statiolake
Copy link
Contributor

Sorry if I misunderstood, but if we want to use matrices for S, we cannot declare IDENTITY as a constant for the first place, no? (Usually we represent a matrix by Vec but we cannot generate the identity matrix at a compile-time.) Since original implementation also uses a function e() for the identity, it would be acceptable cost to declare it as a function.

As an alternative, we can limit the Integral trait implementors to only primitive scalar types or introduce a new trait like ConstIntegral so that we can declare all values as a constant (i.e. replace functions min_value(), max_value(), zero(), one() by constant).

@TonalidadeHidrica
Copy link
Collaborator Author

Thank you. The matrix is indeed good example where we can never use constant. I guess using functions for identity value is the simplest and most reasonable way. @statiolake

@qryxip
Copy link
Member

qryxip commented Sep 12, 2020

@qryxip About monoid trait, that's what I was going to do. Is there any difference between my current implementation and alga?

With alga::general style, we can call binary_operation as a method, and can write fn (foo: &mut Foo, x: impl Monoid<Max>) or something...?

@TonalidadeHidrica
Copy link
Collaborator Author

@qryxip
Which do you think is the better interface? The alga-style or the current style? I'm not got used to Rust enough so it's hard for me to compare.

@qryxip
Copy link
Member

qryxip commented Sep 13, 2020

I'm also not sure. I'm not familiar with those kind of abstraction.

The current implementation also does not seem to cause confliction with the orphan rule.

use lib::{Max, Monoid};

struct Foo;

enum Op {}

impl Monoid for Op {
    type S = Foo;

    fn identity() -> Foo {
        Foo
    }

    fn op(_: &Foo, _: &Foo) -> Foo {
        Foo
    }
}

// This will cause error

//impl Monoid for Max<Foo> {
//    type S = Foo;
//
//    fn identity() -> Foo {
//        Foo
//    }
//
//    fn op(_: &Foo, _: &Foo) -> Foo {
//        Foo
//    }
//}

src/segtree.rs Outdated
pub trait Monoid {
type S: Clone;
fn identity() -> Self::S;
fn binary_operation(a: Self::S, b: Self::S) -> Self::S;
Copy link
Member

@qryxip qryxip Sep 13, 2020

Choose a reason for hiding this comment

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

If we don't require S: Copy, why not change binary_operation(S, S) -> S to 〃(&S, &S) -> S? The binary operations will be inlined.

Copy link
Member

Choose a reason for hiding this comment

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

how about this

pub enum Max<S> {
    __Void(Infallible, PhantomData<fn() -> S>),
}

impl<S: Zero + Copy + Ord + Bounded> Monoid for Max<S> {
    type S = S;

    fn identity() -> S {
        S::zero()
    }

    fn op(lhs: &S, rhs: &S) -> S {
        *lhs + *rhs
    }
}

// Like `num_traits::Zero`.
// https://docs.rs/num-traits/0.2.12/num_traits/identities/trait.Zero.html
pub trait Zero: Sized + Add<Output = Self> {
    fn zero() -> Self;
}

// Like `num_traits::Bounded`.
// https://docs.rs/num-traits/0.2.12/num_traits/bounds/trait.Bounded.html
pub trait Bounded {
    fn min_value() -> Self;
    fn max_value() -> Self;
}

macro_rules! impl_for_primitive_integers {
    ($($ty:ty),*) => {
        $(
            impl Zero for $ty {
                fn zero() -> Self {
                    0
                }
            }

            impl Bounded for $ty {
                fn min_value() -> Self {
                    Self::min_value()
                }

                fn max_value() -> Self {
                    Self::max_value()
                }
            }
        )*
    }
}

impl_for_primitive_integers!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize);

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 design of Zero looks good to me. (Is it redundant if we use the name like AdditiveIdentity or something?)

Changing the signature to take reference instead of value may be good, but then the users should not forget writing & for each argument in trait implementation right? Doesn't it bother? (Yes, it's only a couple-of-bit change)
Also I didn't understand why the binary operations are inlined when references are used. Is there any documentation about it?

Thirdly, why do you always prefer enum Max<S> { __Void(Infallible, PhantomData<fn() -> S>) } to struct Max<S>(PhantomData<fn() -> S>)? I want to know the reason if any.
(BTW I've learned that we should use PhantomData<fn() -> S> rather than PhantomData<S> from this article, which is very unnatural 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.

  1. num_traits::Zero is defined as

    an additive identity

    and users (particularly who are familiar with Rust) will expect that our Zero is so. I think that's enough.

  2. I hadn't thought of that. I thought you were worrying about performance. If we will allow non-Copy large heavy-to-copy data, then we shouldn't force users to clone them. Actually we can take both of S and &S by setting type of the arguments to impl Borrow<S> or something, but I don't think this is idiomatic.

    use std::borrow::Borrow;
    
    fn pass(_: impl Borrow<i32>) {}
    
    pass(0);
    pass(&0);
    pass(&mut 0);

    And

    why the binary operations are inlined when references are used

    Rust inlines functions with small body as other general-purpose programming languages do.

    The Rust Reference

  3. Because the instances of marker types don't need to exist. If a marker type does not take generic parameter, this kind of definition is commonly used.

    enum Marker {}

    But I'm not sure adding Infallible is idiomatic when we need to include PhantomData.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  1. Got it. I'll use the simple name Zero, and additionally One for product identity.
  2. I understood. I'll change the signature later (and that of lazy segtree as well).
  3. I see. If the purpose is to prevent creating instance, then isn't struct Max<S>(Infallible, PhantomData<fn() -> S>) enough?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, probably it's enough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should we change the types of f in max_right and min_left too so that it takes reference as an argument?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think we should change them for consistency.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

* Changed the signature of binary_operation so that it takes references
* Added some default Monoid implementation
@TonalidadeHidrica
Copy link
Collaborator Author

Done fixing. Changing the signature of binary_operation requires additional &'s for users to write...

@TonalidadeHidrica
Copy link
Collaborator Author

Future Task: maybe we should receive Range instead of l: usize, r: usize for prod.

@qryxip
Copy link
Member

qryxip commented Sep 14, 2020

That is good. Then we can also recive range types such as RangeInclusive<usize> (l..=rl..r + 1) in the future without any breaking change.

@TonalidadeHidrica
Copy link
Collaborator Author

Let's do it in the next refactor phase.

@TonalidadeHidrica
Copy link
Collaborator Author

@qryxip Maybe we're ready to merge

@qryxip qryxip mentioned this pull request Sep 16, 2020
@qryxip qryxip merged commit fb9e6c7 into rust-lang-ja:master Sep 17, 2020
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