3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rust RangeBoundsの紹介

Last updated at Posted at 2022-07-17

TLDR

関数などの引数に range を受け取って普通にイテレーションするとき(簡単のため u64 を使っています)

use std::ops::RangeBounds;
use std::iter::IntoIterator;
pub fn sum_n<R: RangeBounds<u64> + IntoIterator<Item=u64>>(range: R) -> u64 {
    let mut result = 0;
    for r in range {
        result += r
    }
    return result
}

普通にイテレーションするわけではなく、 startend の値がほしいとき

use std::ops::{Bound, RangeBounds};
pub fn sum_1<R: RangeBounds<u64>>(range: R) -> u64 {
    let start = match range.start_bound() {
        Bound::Unbounded => 0,
        Bound::Excluded(&s) => s + 1,
        Bound::Included(&s) => s,
    };
    let end = match range.end_bound() {
        Bound::Unbounded => panic!("end_bound should be decided"),
        Bound::Excluded(&t) => t - 1,
        Bound::Included(&t) => t,
    };
    assert!(start <= end);
    (start + end) * (end - start + 1) / 2
}

※ スライスの範囲を指定するように使うときは、end_boundIncluded のほうで +1 することが多いと思います。(そういうときは最大のサイズ max_len がどこかに存在すると思います)

    let end = match range.end_bound() {
        Bound::Unbounded => max_len,
        Bound::Excluded(&t) => t.min(max_len),
        Bound::Included(&t) => (t + 1).min(max_len),
    };

Range 構造体

Range 構造体は、for 文などでよく使っている 0..10 のようなものです。
主には、 std::ops::Range です。

    let mut result = 0;
    for i in 0..10 {
        result += i
    }
    assert_eq!(result, 45)

実は、 Range の他にも RangeFromRangeToInclusive などいろいろあります 。

start..endstart....=end などはそれぞれ違う構造体で表現されているんですね。

RangeBounds トレイト

Range 構造体の他にも RangeFrom 構造体や RangeToInclusive 構造体などがありました。しかし、これらは全て範囲を表すオブジェクトなので、共通するインターフェースが用意されていると嬉しいですよね。それが RangeBounds トレイトです。

Range 構造体や RangeFrom 構造体、 RangeToInclusive 構造体などにはこの RangeBounds トレイトが実装されています。

総和を計算する

RangeBounds トレイトの使用例として、受け取った範囲の和を返す関数を作成してみます。

use std::iter::IntoIterator;
use std::ops::RangeBounds;
pub fn sum_n<R: RangeBounds<u64> + IntoIterator<Item = u64>>(range: R) -> u64 {
    let mut result = 0;
    for r in range {
        result += r
    }
    result
}

foldなど使えばfor文を使わずに書けますが、ここではfor文を使って書いておきます。
参考:

この sum_n 関数は、次のように使うことができます。

fn main() {
    assert_eq!(sum_n(1..10), 45);
    assert_eq!(sum_n(1..=10), 55);
}

しかし、この関数にはいくつか問題があります。..end のように書いたときの RangeTo などの構造体は、RangeBounds は実装しているものの、Iterator は実装していません。当然と言えば当然ですが、始点の情報がないのでイテレーションすることができません。次のように書くとコンパイルエラーとなってしまいます。

fn main() {
    assert_eq!(sum_n(..10), 45);
}
error[E0277]: `RangeTo<u64>` is not an iterator
 --> src/main1.rs:2:22
  |
2 |     assert_eq!(sum_n(..10), 45);
  |                ----- ^^^^ if you meant to iterate until a value, add a starting value
  |                |
  |                required by a bound introduced by this call
  |
  = help: the trait `Iterator` is not implemented for `RangeTo<u64>`
  = note: `..end` is a `RangeTo`, which cannot be iterated on; you might have meant to have a bounded `Range`: `0..end`
  = note: required because of the requirements on the impl of `IntoIterator` for `RangeTo<u64>`

また、単純に前から順に足していっているので、最初の値 start と最後の値 end が離れていると、当然処理に時間がかかってしまいます。さらには、end を指定しない、start.. のように書いたときの RangeFrom 構造体を渡してしまうと、この関数は、リリースビルドでは、無限ループに陥ってしまいます(デバッグビルドでは加算のオーバーフローでプログラムが終了します)。

イテレーションするだけなら RangeBounds は用いる必要がないですが、引数が Range (など)であってほしい、ということを型のレベルで表現することができるので、望まない入力をコンパイルエラーにできることはうれしいですね。

総和を効率化する

さて、先ほどの sum_n 関数では、最初の値 $s$ と最後の値 $t$ が大きく離れていると、計算に時間がかかってしまいました。幸いなことに、皆さんご存じの通り、 $s$ から $t$ までの和を計算する公式があります。
$$
\sum_{i=s}^{t} i = \frac{(s + t)(t - s + 1)}{2}
$$
先ほどの方法では時間計算量は $O(t-s)$ ですが、この方法であれば $O(1)$ です。どれだけ離れた $s$ と $t$ を持ってきても計算時間は(理論上)変わらないのでうれしいです。
これはRustのプログラムでは、次のsum_1 関数のように書けます。

use std::ops::{Bound, RangeBounds};
pub fn sum_1<R: RangeBounds<u64>>(range: R) -> u64 {
    let start = match range.start_bound() {
        Bound::Unbounded => 0,
        Bound::Excluded(&s) => s + 1,
        Bound::Included(&s) => s,
    };
    let end = match range.end_bound() {
        Bound::Unbounded => panic!("end_bound should be decided"),
        Bound::Excluded(&t) => t - 1,
        Bound::Included(&t) => t,
    };
    assert!(start <= end);
    (start + end) * (end - start + 1) / 2
}

なんか長いですね、そのあたりについては最後に少し詳細を書きたいと思います。やっていることは単純で、RangeBound トレイトによって提供される start_bound メソッドと end_bound メソッドで最初と最後の値を持ってきているだけです。ただし、..end の場合と ..=end の場合で、end の値は 1 多かったり少なかったりします。それを表すのが Bound::ExcludedBound::Included ですね。 Bound::Unbounded は単純に値が指定されていないことを表します。 start についても Excluded が用意されていますが、普通に使っている限りは startExcluded になることは無いと思います(標準では用意されていないので)。

この sum_1 関数は、次のように使うことができます。

fn main() {
    assert_eq!(sum_1(1..10), 45);
    assert_eq!(sum_1(1..=10), 55);
    assert_eq!(sum_1(..10), 45);
    // println!("{}", sum_1(1..)); // panic!
}

最後にコメントアウトしている行は、range の最後を指定していないため、panic! するように sum_1 関数で記述されています。ここで適当な上限の値を好きに書いておけば、panic! を避けることもできます。range の最後を指定しないとき、初めの sum_n 関数では無限ループに陥ってしまっていましたが、この方法を使えば無限ループは避けることができます。

スライスの範囲を指定する場合

さて、ここまでは単純に range の和を扱ってきましたが、range はスライスの範囲を指定する用途で使用されることも多いと思います。

fn main() {
    let v = vec![1, 2, 3, 4];
    println!("{:?}", &v[1..3]) // [2, 3]
}

このようなものを表現したいときは、先ほどの sum_1 関数とは、end_bound の1足したり引いたりが少し変わってくると思います。

use std::ops::{Bound, RangeBounds};
pub fn range_decide<R: RangeBounds<usize>>(range: R, len: usize) -> (usize, usize) {
    let start = match range.start_bound() {
        Bound::Unbounded => 0,
        Bound::Excluded(&s) => s + 1,
        Bound::Included(&s) => s,
    };
    let end = match range.end_bound() {
        Bound::Unbounded => len,
        Bound::Excluded(&t) => t.min(len),
        Bound::Included(&t) => (t + 1).min(len),
    };
    assert!(start <= end);
    (start, end)
}
fn main() {
    let max_len = 100;
    assert_eq!(range_decide(5..10, max_len), (5, 10));
    assert_eq!(range_decide(5..=10, max_len), (5, 11));
    assert_eq!(range_decide(0.., max_len), (0, 100));
    assert_eq!(range_decide(..10, max_len), (0, 10));
    assert_eq!(range_decide(..=10, max_len), (0, 11));
    assert_eq!(range_decide(.., max_len), (0, 100));
    assert_eq!(range_decide(5..5, max_len), (5, 5));
}

半開区間で表したいときはこちらの方がいいですね。

なんか長いがなぜか

スライスの範囲を指定するときと同じ様子で end_bound に1を足す std::slice::range メソッドが、標準で用意されています(2022/07/17 現在 nightly-only ですが…)。

この関数は、第一引数に任意の RangeBounds<usize> を指定し、第二引数には RangeTo のオブジェクト(..end のような形のもの)を指定します。usize 限定ではありますが、使用例としては次のようになります。

#![feature(slice_range)]
fn main() {
    let max_len = 100;
    let range = std::slice::range(..10, ..max_len);
    assert_eq!((range.start, range.end), (0, 10));
}

このあたりについては GitHub のこのあたりの issue でやり取りされているようです。

まとめ

range 系の構造体を一括で扱うことができるトレイト RangeBounds を紹介しました。

このトレイトを用いることで、関数の引数などで、範囲を指定することができます。
単純にイテレーションしたい場合には別途 IntoIter なども求めることになりますが、RangeBounds を用いることで、range であることを指定できるのはうれしいですね。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?