9
6

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 5 years have passed since last update.

D言語Advent Calendar 2016

Day 14

D言語で今楽しむRange

Posted at

#はじめに

この記事はD言語で今始めるRangeの続きとなるものです。
前回と同じく、D言語初心者向けです。
特殊なテクニック解説というより、phobosに用意されているレンジに関する関数の紹介記事です。

#D言語で今楽しむRange

前の記事では、レンジという概念を作るモチベーションと、その面白さについてお話ししました。僕が挙げた例はたった3つでしたが、実際にはたくさんの関数がphobosに用意されています。今回はそのたくさんの関数のうち特によく使うと思われるいくつかを取り上げ、実際にどのように使うのか、どのように楽しめるかを紹介します。

#まず知っておきたいこと

これは別にレンジの関数に限ったことじゃないのですが、ライブラリのリファレンスを読んでいる初心者(つまり僕)が困惑する可能性のある点がありますので、ここでそれを解いておこうと思います。(つまり、公式リファレンス1を読もうということです!)

##引数、返り値の型

レンジという共通の概念で理解できること、つまりレンジという概念さえ知っていればよいことは非常に大きなメリットです。レンジを使いたい人―例えば、要素をすべて足した結果を得たい人など―は、レンジの真の型(構造体Foo?Bar?いやクラスか?それとも配列?)を知る必要はないのでした。彼はそのレンジがInputRangeRandomAccessRangeかなど、種類さえわかれば十分なのです。
そこで、これから紹介する関数はリファレンスをたどると多くが返り値の型をautoRなどとごまかしていますが、気にしないでください。なぜならそのauto,Rの実体が何かなどを知る必要はないからです。
また、引数も多くがとても抽象的な指定がなされています。もちろん、これはいろいろな型のレンジを受け取れるようにするためです。
つまり、型を確認するよりもどの種類のレンジが返ってくるのか引数にどのレンジを要求しているのかをしっかり確認してください。

#レンジを扱う三種類

そういえば、前の記事を読んでいただいた方は、三つのものを作ったことを覚えているでしょうか?

  • 漸化式を表すレンジ
    (引数からレンジを生成する)
  • sum関数
    (レンジを使って計算する)
  • skip関数
    (レンジを変化させる)

これらは明らかに、それぞれ異なる働きをしていました。これを踏まえて

  • レンジの生成
  • レンジの利用
  • レンジの変化

の三つに分類して紹介しようと思います。2

#レンジを生成する

レンジの世界へ入る起点となる関数たちです。

##iota

0,1,2,3,...というシンプルな数のレンジを生成します。
例えば、iota(5)は、0,1,2,3,4を表すレンジです。開始、終了、ステップを指定でき、またいろいろな型で使えます。

import std.range : iota;

unittest {
    import std.algorithm : equal;
    assert(iota(10).equal([0,1,2,3,4,5,6,7,8,9]));      //一引数のとき
    assert(iota(4,10).equal([4,5,6,7,8,9]));            //二引数のとき
    assert(iota(0,10,2).equal([0,2,4,6,8]));            //三引数のとき

    import std.algorithm : sum;
    assert(iota(10).sum == 45);
}

unittest {
    import std.bigint;
    import std.algorithm : sum;
    assert(iota(BigInt(10)).sum == BigInt(45));         //BigIntでも使えるぞ!
}

##repeat

読んで字の如く、同じ値を繰り返すレンジを作ります。
無限レンジとしても、個数を指定しても使えます。

import std.range : repeat;

unittest {
    assert(repeat(3,5).equal([3,3,3,3,3]));

    import std.range : take;
    import std.algorithm : equal;
    assert(repeat(3).take(8).equal([3,3,3,3,3,3,3,3]));
}

#レンジを利用する

レンジの中身を実際に利用する関数たちです。

##fold

二引数の関数を次々に適用して、一つの値に畳み込みます。

import std.algorithm : fold;

unittest {
    //max(1,5), max(5,2), ...とmaxを次々と適用し、最大値を得ます。
    import std.algorithm : max;
    assert([1,5,2,7,3,0].fold!max == 7);
}

fold!に続くテンプレート引数は基本的に二引数関数を指定します。
例えば上記の例ではまずmax(1,5)が呼ばれ、その結果である5と、さらに次の要素である2を用いてmax(5,2)が呼ばれ、その結果である5とさらに次の要素・・・と、要素が畳み込まれていきます。

###関数の渡し方

これはレンジの関数に限ったことではないですが、テンプレート引数として関数を渡す場合があります。この渡し方が(おそらく)D言語独特なので、ここで触れておきましょう。主に考えられる渡し方は三つです。

####単純に関数を渡す

自分で作るなり、既存のものを使うなり、とりあえず名前を書けば指定できます。

import std.algorithm : fold;

unittest {
    auto result = [1,2,3,4,5].fold!tasu;
    assert(result == 15);
}

int tasu(int a, int b) { return a + b; }

####関数リテラルを直接渡す

関数をわざわざ別の場所に書くのではなく、関数を使うまさにその場所に書くこともできます。関数リテラルの記法については詳しく書きませんが、主に二通りの書き方ができます。

import std.algorithm : fold;

unittest {
    auto result1 = [1,2,3,4,5].fold!(
            (a,b) { return a + b; }
            );
    assert(result1 == 15);

    auto result2 = [1,2,3,4,5].fold!(
            (a,b) => a + b
            );
    assert(result2 == 15);
}

####文字列で渡す

最も困惑されやすいのがこの記法だと思います。D言語では、文字列から関数を生成するテンプレートが存在します。具体的には、std.functionalにあるunaryFunbinaryFunがそうです。
これらは与えられたテンプレート引数が関数ならそのまま素通りさせ、文字列なら指定した文字を引数のようにみなして関数を生成してくれます。つまり、

import std.functional : unaryFun, binaryFun;

unittest {
    alias myIncrement = unaryFun!"a + 1";
    alias myPlus = binaryFun!"a + b";

    assert(myIncrement(1) == 2);
    assert(myPlus(1,2) == 3);
}

のように書けます。このabはこのテンプレートのデフォルトの設定から来ています3
つまり、foldにはこんな書き方もできますね。

import std.algorithm : fold;

unittest {
    assert([1,2,3,4,5].fold!"a + b" == 15);
}

##each

eachはこの種の関数の中でも少し特殊で、返り値がありません。ある関数に要素を次々と渡し、実行したら返り値は気にせずおしまい!という関数です。
僕はこれをwritelnと組み合わせて使うのが好きです。

import std.algorithm : each;

void main() {
    import std.stdio : writeln;
    [1,2,3,4,5]
        .each!writeln;
}

.each!writelnと後ろに書くだけで要素を全部表示してくれます!
もうひとつよく使うのは、.eachをそのまま書きその時点でレンジを評価して使い切るという使い方です。こうすることで、自分で好きな時にレンジの計算を行わせ、時間を計測したりできます。

import std.algorithm : each;

void main() {
    import std.range : iota;
    import std.stdio : writeln;
    import std.datetime : benchmark;
    auto bench = benchmark!(() => iota(10000).each)(1000);

    //何ミリ秒かかるか見ます。(配列で結果がもらえるので全部表示するのにeachしましょう!)
    bench.each!((t) => t.msecs.writeln);
}

##equal

前回の記事にも書きましたが、配列とレンジとか、レンジと他の種類のレンジとか、違う型同士の比較は普通できません4。しかしこの関数を使えば、要素を一つずつ丁寧に比較してくれますので、要素同士が比較可能なら使うことができます。

import std.algorithm : equal;
import std.range : iota;

unittest {
    //assert(iota(5) == [0,1,2,3,4]);       //error! (iotaの)Resultとint[]は比較できない!
    assert(iota(5).equal([0,1,2,3,4]));     //それぞれの要素は比較可能
}

さらにこのequalは比較方法をテンプレート引数で指定できます。これはレンジを要素にもつレンジを比較するときなどに便利です。

import std.algorithm : equal;
import std.range : iota;

unittest {
    //assert([iota(3), iota(5)].equal([[0,1,2], [0,1,2,3,4]]));     //error! 各要素はResultとint[]のままなので比較できない!
    assert([iota(3), iota(5)].equal!equal([[0,1,2], [0,1,2,3,4]])); //レンジの各要素をさらにequalで比較します。
}

#レンジを変化させる

レンジを受け取って、何らかの操作を加えたレンジを返す関数たちです。

##map

それぞれの要素に関数を適用して新たなレンジにします。
非常に汎用性の高い関数で、いろんなところで使えます。

import std.algorithm : map;

unittest {
    import std.range : iota;
    import std.algorithm : equal;
    assert(iota(5).map!"a ^^ 2".equal([0,1,4,9,16]));    //それぞれの要素を二乗します。
    auto rangeOfRange = iota(5).map!((x) => iota(x));   //レンジのレンジを作ることも簡単にできます!
}

##filter

要素のうち条件に合致したものだけを抽出します。

import std.algorithm : filter;

unittest {
    import std.range : iota;
    import std.algorithm : equal;
    //3の倍数か5の倍数のものだけを抽出します。
    assert(iota(30).filter!((x) => x % 3 == 0 || x % 5 == 0)
            .equal([0,3,5,6,9,10,12,15,18,20,21,24,25,27]));
}

#実際に使って遊ぼう

##0から9までの数の二乗の和を求める

import std.stdio;
import std.range;
import std.algorithm;

void main() {
    iota(10)
        .map!((x) => x^^2)
        .sum
        .writeln;
}

##9999までの数の二乗の和を求める

BigIntと組み合わせると大きい数でも扱えます。

import std.stdio;
import std.range;
import std.algorithm;

void main() {
    import std.bigint;
    iota(BigInt(10000))
        .map!((x) => x ^^ 2)
        .sum
        .writeln;
}

##二次関数のグラフを作ってみる

repeatを使ってなんちゃってグラフ。

import std.stdio;
import std.range;
import std.algorithm;

void main() {
    iota(15)
        .map!((x) => x ^^ 2)
        .map!((x) => repeat('|', x))
        .each!writeln;
}

#まとめ
全部書こうとするときりがないのでこのへんにしておきますが、phobosにはほかにもたくさん関数が用意されていますので、ぜひ公式リファレンス1は目を通しておくことをお勧めします。
また今回の記事はphobosの機能の紹介となっていますが、自分自身でこれらを簡単に作れることがレンジの素晴らしい点でもあります。自作関数も織り交ぜて、複雑な計算をぽんとこなすのは実に気持ちがいいものです。ぜひどうぞ!

  1. std.rangestd.algorithmなど。 2

  2. この分類は僕が勝手に考えたものです。

  3. このa,bはテンプレートの第二、第三引数によって自分で変えられます。

  4. 演算子オーバーロード(なんと今年のACで取り上げられている!)などにより、それぞれ対応することはできます。

9
6
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
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?