技術的なやつ

技術的なやつ

0.2 OAuth認証(PINコード)

計算機科学実験及び演習4で、C#を用いたTwitter REST APIを利用するライブラリ制作を行った。
OAuth認証についてはライブラリを用いて良いということだったが、Twitter用にOAuth認証するライブラリは大体タイムライン取得やつぶやきも出来るようになってて、あまり意味が無いと感じたので、Google先生が提供しているOAuthBaseを利用して、割と一から認証処理を書いた。
おかげで、OAuth認証が何をやっているのかが分かったので、メモ書き程度の記事にしてみようと思う。
なお、今回扱うOAuth認証はPINコードを用いるもので、バージョンは1.0である。

OAuth1.0(PINコード)の流れ(見た目)

ブラウザアプリの場合は必要ないので、あまり見かけることはないが、数字を入力させる認証方法である。
具体的には、Twitterでは以下の様な流れになる。

  1. ブラウザ立ち上げ
  2. Twitterにログイン
  3. アプリと連携させるか選択
  4. PINコード(7桁の数字)を表示する
  5. アプリ内にPINコードを入力
  6. 認証完了

これが内部で何を行っているのかを探る。

OAuth1.0(PINコード)の流れ(中身)

まず、OAuth認証で重要なsignatureについて述べる。

signature

signatureはBase64形式のハッシュ値である。サーバとクライアントでこの値が一致していれば認証が通る。
signatureは、基礎値となるsignature_base文字列と、鍵となるkey文字列をHMAC-SHA1でハッシュ化したものをさらにBase64に変換した文字列となっている。

signature_base

基礎値となる文字列は、以下の情報を基に生成される。

  • URL(パラメータ含む)
  • consumer_key
  • access_token
  • timestamp
  • nonce

consumer_keyはアプリケーションが持つ鍵の一つである。access_tokenはユーザが各自持つ鍵の一つである。nonceはランダム文字列である。

key

keyは、以下の情報を基に生成される。

  • consumer_secret
  • access_token_secret

これらを繋げてハッシュ化したものが鍵となる。

TwitterにおけるOAuth認証の流れ

request_token

ユーザは、アプリケーションの持つconsumer_keyおよびconsumer_secretしか情報を持っていない。よって、まずはaccess_tokenおよびaccess_token_secretを取得する必要がある。
そこで、access_tokenおよびaccess_token_secretを空の文字列としたsignatureをTwitter側に送信し、Twitterが生成したaccess_tokenおよびaccess_token_secretを受け取る。
signatureの受け渡しでは、同時に基礎文字列の材料も全てパラメータで受け渡す。つまり、パラメータさえ見ればsignature_baseは簡単に求められる。しかし、鍵の基となる情報は送受信しないため(Twitterとユーザが個人で持っている)、安全性が保証されるわけである。
この段階は、「そのアプリケーションに、渡したaccess_tokenおよびaccess_token_secretを持つ認証待ちユーザがいる」という意味を持つ。

TwitterログインとPINコード入力

ここで、ユーザにはTwitterにログインしてもらう。ログイン後、ユーザがアプリ連携を認めると、7桁のPINコードが表示される。アプリケーションは、このPINコードを受け取る。

access_token

先ほど受け取ったaccess_tokenおよびaccess_token_secretは、request_tokenおよびrequest_token_secretと呼ぶ。これらのリクエストトークンを用いて再びsignatureを求める。このsignatureと、先ほど受け取ったPINコードを、再びTwitterに送信し、Twitterから新しいaccess_tokenおよびaccess_token_secretを受け取る。これがAPIを叩くときのOAuth認証に使われるトークンとなる。
この段階は、「Twitterでログインされた、確かなユーザが認証された」という意味を持つ。

APIを叩く

APIを叩く際は、signature_baseの生成に必要な情報と、signatureをTwitterに送信する。
先ほどと同様、signature_baseはパラメータから簡単に生成できるが、signature生成のための鍵はお互い送受信してないデータを用いて生成するため、安全性が保証されるわけである。

雑感

要するに、

  • signature(signature_baseとkeyを使ったハッシュ)を照合させて認証する
  • signature_baseは固定文字列とタイムスタンプとランダム文字列を用いて生成される
  • keyはサーバ・クライアントがお互いローカルに保持する2つの文字列を用いて生成される
  • 2つの鍵は、アプリケーションの鍵とユーザの鍵
  • この2段階認証によって特定のアプリ上の特定のユーザが認証できる

というわけであるが、access_token_secretは最初の一回はサーバ側からクライアント側に送信される必要があるわけで、少し微妙な認証方式だと思う。

Rubybot作ってた時は、意味もわからずconsumer_key・consumer_secret・access_token・access_token_secretを入力していたが、今回の実験でその利用法が分かって良かった。

全然関係ない話になるが、RSA暗号はこの「鍵配送問題」を解決した画期的な暗号である。次回の記事で解説しようと思う。

4.2 Rubyでマルチプロセス

コンちゃんの改良にあたってマルチプロセスないしマルチスレッドの機構が必要になったのでメモ。
Ruby(テスト環境ではRuby1.9.3)でマルチプロセスをしたい時は、以下のように書く。

Process.fork do
  # ここに処理を書く
end

Process.forkで囲んだところに、子プロセスの処理を書く。

以下、テストコードとその結果。

Process.fork do
  10.times do
    puts "Chinchin!!!"
    sleep 1
  end
end

5.times do
  puts "Hello!!!!"
  sleep 1
end
Hello!!!!
Chinchin!!!
Hello!!!!
Chinchin!!!
Hello!!!!
Chinchin!!!
Hello!!!!
Chinchin!!!
Hello!!!!
Chinchin!!!
Chinchin!!!
okabi@ibako:~/$ Chinchin!!!
Chinchin!!!
Chinchin!!!
Chinchin!!!

親プロセスが死ぬと端末的にはプロセス全体が終了としたと見なすらしい。

テスト環境はvbox上のUbuntu12.04だが、低スペにしたからか日本語入力処理が遅すぎて困る。デスクトップ買うか組むかしてLinux専用機作りたいでござる。

5.7 CROSSO的採点機

  1. Unityを用いる
    1. C#について少し復習する
    2. Unityの2D描画について簡単に学ぶ
  2. Unityの音声制御
    1. MIDIを再生させる手段を見つける
    2. 音声のマイク入力手段を見つける
  3. 入力音声の解析
    1. UnityのFFTライブラリを触ってみる
    2. FFTされたデータから音程を確定させるアルゴリズムを考える
  4. MIDIの解析
    1. 第1トラックの楽譜を解析する
    2. MIDIの再生箇所を解析する手段を見つける
  5. 採点のための解析
    1. 全体の点数計算アルゴリズムを考える
    2. ビブラート判定アルゴリズムを考える
    3. しゃくり判定アルゴリズムを考える
    4. タイミング判定アルゴリズムを考える
    5. なめらかさ判定アルゴリズムを考える

f:id:ibako31:20140303005448j:plain

CROSSO風な採点機が完成した。

配点

CROSSOなので、こんな感じ。

  • 音程…90
    • ただし、線形配点ではない
  • タイミング…2
  • なめらかさ…1
  • ビブラート…7
  • しゃくり…3

音程

瞬間瞬間で、歌唱した数値音階と、その時出すべき数値音階を渡して評価する。
ぴったりじゃなくても部分点が入るようにしている。

タイミング

1つのノートが終わるごとに評価する。
4分音符以下の長さの音程バーのどこかで、正しい音程を発していればOK。
音程バーにはそれぞれタグが付いており、タグの数値はx分音符を意味する。たとえば、四分音符なら4、全音符なら1、タイで2小節にまたがる全音符・全音符なら0.5。

なめらかさ

2つのノートが終わるごとに評価する(事実的に1つのノートが終わるごとに評価する)。
2つのノートの間で何らかの音を発していればOK。階段上に音が続く部分では特に大きく評価する。

ビブラート

1つのノートが終わるごとに評価する。
そのノート中に波が2つ以上連続して存在すればOK。
sin的な波と-sin的な波の2つを想定する。評価ポイントは、sin(x)でいうとx=0,PI/2,PI,3PI/2,2PI。横幅および縦幅に制限を設け、それを満たしていればOKとする。

しゃくり

1つのノートが終わるごとに評価する。
ノート中に、「本来の音程より低い音程をm秒連続で発し、その後正しい音程をn秒保った」場合OK。

コード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CROSSO
{
	class Evaluator
	{
        private int all_time;  // 歌唱しなければならない全時間(10ms)
        private int time;  // 歌唱時間(10ms)
        class Interval
        {
            private int interval_score;  // 音程点。満点なら1回の評価につき5点入る
            private int interval_num;  // 評価した音程の数
            
            // 初期化処理
            public Interval()
            {
                interval_score = 0;
                interval_num = 0;
            }

            // 音程を1回評価する
            // scale: 出した数値音階、best_scale: 出すべき数値音階
            public void Evaluate(float scale, float best_scale)
            {
                float dif = Math.Abs(scale - best_scale);
                interval_num += 1;
                if (scale == 0.0f)
                {
                    interval_score += 2;
                }
                else if (dif < 0.7f)
                {
                    interval_score += 5;
                }
                else if (dif < 1.0f)
                {
                    interval_score += 4;
                }
                else if (dif < 1.5f)
                {
                    interval_score += 3;
                }
                else if (dif < 2.0f)
                {
                    interval_score += 2;
                }
                else
                {
                    interval_score += 1;
                }
            }

            // 音程の評価値を返す
            public float GetScore()
            {
                if (interval_num == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (interval_score / 5.0f / interval_num);
                }
            }
        }


        class Timing
        {
            private int timing_score;  // 評価点。0か1か
            private int timing_num;  // 評価したタイミングの数
            
            // 初期化処理
            public Timing()
            {
                timing_score = 0;
                timing_num = 0;
            }

            // タイミングを1回評価する
            // scale: 出した数値音階、best_scale: 出すべき数値音階、start_index: 始まりのインデックス番号、index_size: その音符のインデックス長さ
            public void Evaluate(float[] scale, float best_scale, int start_index, int index_size)
            {
                timing_num += 1;
                for (int i = 0; i < index_size; i++)
                {
                    float dif = Math.Abs(scale[(start_index + i) % scale.Length] - best_scale);
                    if (scale[i] != 0.0f)
                    {
                        if (dif < 0.5f)
                        {
                            timing_score += 1;
                            break;
                        }
                    }
                }
            }

            // タイミングの評価値を返す
            public float GetScore()
            {
                if (timing_num == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (timing_score / 1.0f / timing_num);
                }
            }
        }


        class Smoothness
        {
            private int smoothness_score;  // 評価点。0~5
            private int smoothness_num;  // 評価したなめらかさの数
        
            // 初期化処理
            public Smoothness()
            {
                smoothness_score = 0;
                smoothness_num = 0;
            }

            // なめらかさを1回評価する(合わせて全音符以下の長さの、2つ分のノートを評価する)
            // scale: 出した数値音階、best_scale: 出すべき数値音階(2つ)、start_index: 始まりのインデックス番号、index_size: インデックス長さ、border_index: 境界のインデックス
            public void Evaluate(float[] scale, float[] best_scale, int start_index, int index_size, int border_index)
            {
                smoothness_num += 1;
                for (int i = 0; i < index_size; i++)
                {
                    int index = (start_index + i) % scale.Length;
                    if (index == border_index % scale.Length)
                    {
                        if (scale[index] != 0.0f)
                        {
                            float dif = Math.Abs(best_scale[0] - best_scale[1]);
                            if (dif == 0.0f) smoothness_score += 1;
                            if (dif <= 2.0f) smoothness_score += 5;
                            else if (dif <= 5.0f) smoothness_score += 3;
                            else smoothness_score += 1;
                        }
                        else
                        {
                            smoothness_score += 0;
                        }
                        break;
                    }
                }
            }

            // なめらかさの評価値を返す
            public float GetScore()
            {
                if (smoothness_num == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (smoothness_score / 3.0f / smoothness_num);
                }
            }
        }


        class Vibrato
        {
            private int vibrato_score;  // 評価点。感知できたノート数
            private int vibrato_num;  // 評価したノートの数
            private int time;  // 歌唱時間
            
            // 初期化処理
            public Vibrato()
            {
                vibrato_score = 0;
                vibrato_num = 0;
                time = 0;
            }

            // ビブラートを1回評価する
            // scale: 出した数値音階、start_index: 始まりのインデックス番号、index_size: その音符のインデックス長さ、time: 歌唱時間
            // 戻り値: 0-> 感知されなかった、1-> 感知された
            public int Evaluate(float[] scale, int start_index, int index_size, int time)
            {
                this.time = time;
                const int necessary_wave_num = 2;  // 感知に必要な波の数
                const int limit_time = 50;  // ビブラート感知の限界時間(*10ms)
                const float limit_height_min = 0.5f;  // ビブラート感知可能な限界最低縦幅(数値音階)
                vibrato_num += 1;
                int break_flag = 0;

                for (int i = 0; i < index_size; i++)
                {
                    int base_index = (start_index + i) % scale.Length;  // ビブラートの起点とするインデックス
                    float base_scale = scale[base_index];  // 起点の数値音階
                    // 発音されてない場合は無効
                    if (scale[base_index] == 0.0f) continue;

                    // 感知の段階。n:now, b:base_scale, h:limit_height_min
                    // mod 4 について。必要感知波数を変えられるように。 
                    // 0-> n >= b、 1-> n >= b+h、 2-> n <= b、 3-> n <= b-h
                    // 0-> n <= b、-1-> n <= b-h、-2-> n >= b、-3-> n <= b-h
                    int process = 0;

                    for (int j = 1; j < limit_time && i + j < index_size; j++)
                    {
                        int index = (base_index + j) % scale.Length;
                        float dif = scale[index] - base_scale;  // 起点の数値音階との差
                        int process_mod = Math.Abs(process) % 4;

                        // 発音されてない場合は無効
                        if (scale[index] == 0.0f) break;

                        // sin的な波
                        if (process >= 0)
                        {
                            if (process_mod == 0)
                            {
                                if (dif >= limit_height_min) process++;
                            }
                            else if (process_mod == 1)
                            {
                                if (dif <= 0) process++;
                            }
                            else if (process_mod == 2)
                            {
                                if (dif <= -limit_height_min) process++;
                            }
                            else
                            {
                                if (dif >= 0) process++;
                            }
                        }
                        // -sin的な波
                        else
                        {
                            if (process_mod == 0)
                            {
                                if (dif <= -limit_height_min) process--;
                            }
                            else if (process_mod == 1)
                            {
                                if (dif >= 0) process--;
                            }
                            else if (process_mod == 2)
                            {
                                if (dif >= limit_height_min) process--;
                            }
                            else
                            {
                                if (dif <= 0) process--;
                            }
                        }

                        // 波を2つ感知できていたらOK!
                        if (Math.Abs(process) == 4 * necessary_wave_num)
                        {
                            vibrato_score++;
                            break_flag = 1;
                            break;
                        }
                    }
                    if (break_flag == 1) break;
                }

                if (break_flag == 0) return 0;
                else return 1;
            }

            // ビブラートの評価値を返す
            public float GetScore()
            {
                if (time == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (3.0f * (float)vibrato_score / ((float)time / 100.0f));
                }
            }
        }


        class Singup
        {
            private int singup_score;  // 評価点。感知できたノート数
            private int singup_num;  // 評価したノートの数
            private int time;  // 歌唱時間
            
            // 初期化処理
            public Singup()
            {
                singup_score = 0;
                singup_num = 0;
                time = 0;
            }

            // しゃくりを1回評価する
            // scale: 出した数値音階、best_scale: 出すべき数値音階、start_index: 始まりのインデックス番号、index_size: その音符のインデックス長さ、time: 歌唱時間
            // 戻り値: 0-> 感知されなかった、1-> 感知された
            public int Evaluate(float[] scale, float best_scale, int start_index, int index_size, int time)
            {
                this.time = time;
                const int necessary_time = 5;  // しゃくり感知のために必要な時間(*10ms)
                const int limit_time = 30;  // しゃくり感知の限界時間(*10ms)
                const int necessary_stable_time = 5; // しゃくり感知のために必要な、正しい音程での安定時間(*10ms)
                singup_num += 1;

                int ok = 0;  // しゃくり感知したら1
                int process = -1;  // -1:音程が下になるまで待つ、0:音程のずり上がり中、それ以外:正しい音程での安定中(数値は正しい音程に達した時間)
                int start_time = 0;  // 低音を出し始めた時間

                for (int i = 0; i < index_size && i < limit_time; i++)
                {
                    int index = (start_index + i) % scale.Length;  // チェックするインデックス
                    if (process == -1 && scale[index] == 0.0f) break;  // 発音していない部分があれば終了
                    else if (best_scale - 0.5f <= scale[index] && scale[index] <= best_scale + 0.5f)  // 正しい音程
                    {
                        if (process == 0)
                        {
                            if (i - start_time >= necessary_time - 1)
                            {
                                process = i;
                            }
                            else if (i - start_time >= limit_time)
                            {
                                break;
                            }
                        }
                        else if (process > 0)
                        {
                            if (i - process >= necessary_stable_time)
                            {
                                singup_score++;
                                ok = 1;
                                break;
                            }
                        }    
                    }
                    else if (process > 0) break;  // 一旦正しい音程に達したのに、安定して正しい音程が出せなかった
                    else if (scale[index] < best_scale - 0.5f)  // 正しい音程より低い音を出している
                    {
                        process = 0;
                        start_time = i;
                    }
                    else break;
                }

                return ok;
            }

            // しゃくりの評価値を返す
            public float GetScore()
            {
                if (time == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (3.0f * (float)singup_score / ((float)time / 100.0f));
                }
            }
        }


        private Interval interval;
        private Timing timing;
        private Smoothness smoothness;
        private Vibrato vibrato;
        private Singup singup;


        // 最初に実行する
        public Evaluator(int all_time)
        {
            this.all_time = all_time;
            time = 0;
            interval = new Interval();
            timing = new Timing();
            smoothness = new Smoothness();
            vibrato = new Vibrato();
            singup = new Singup();
        }

        // 音程評価のために実行する。瞬間瞬間で回す
        // 歌唱時間の計算も行う
        // scale: 出した数値音階、best_scale: 出すべき数値音階
        public void EvaluateInterval(float scale, float best_scale)
        {
            interval.Evaluate(scale, best_scale);
            time += 1;
        }

        // タイミング評価のために実行する。1つのノートが終わったら実行する
        // scale: 出した数値音階の配列、best_scale: 出すべき数値音階、start_index: 出した数値音階の配列のスタートインデックス、index_size: 評価すべきインデックス数
        public void EvaluateTiming(float[] scale, float best_scale, int start_index, int index_size)
        {
            timing.Evaluate(scale, best_scale, start_index, index_size);
        }

        // なめらかさ評価のために実行する。2つのノートが終わったら実行する
        // scale: 出した数値音階の配列、best_scale: 出すべき数値音階(2つ)、start_index: 出した数値音階の配列のスタートインデックス、index_size: 評価すべきインデックス数、border_index: 境界インデックス
        public void EvaluateSmoothness(float[] scale, float[] best_scale, int start_index, int index_size, int border_index)
        {
            smoothness.Evaluate(scale, best_scale, start_index, index_size, border_index);
        }

        // ビブラート評価のために実行する。1つのノートが終わったら実行する
        // scale: 出した数値音階の配列、start_index: 出した数値音階の配列のスタートインデックス、index_size: 評価すべきインデックス数
        // 戻り値: 0-> 感知されなかった、1-> 感知された
        public int EvaluateVibrato(float[] scale, int start_index, int index_size)
        {
            return vibrato.Evaluate(scale, start_index, index_size, time);
        }

        // しゃくり評価のために実行する。1つのノートが終わったら実行する
        // scale: 出した数値音階の配列、best_scale: 出すべき数値音階、start_index: 出した数値音階の配列のスタートインデックス、index_size: 評価すべきインデックス数
        // 戻り値: 0-> 感知されなかった、1-> 感知された
        public int EvaluateSingup(float[] scale, float best_scale, int start_index, int index_size)
        {
            return singup.Evaluate(scale, best_scale, start_index, index_size, time);
        }

        // 評価値を得る
        // id -> 0: 総合, 1: 音程, 2: タイミング, 3: なめらかさ, 4: ビブラート, 5: しゃくり
        public float GetScore(int id)
        {
            if (id == 0)
            {
                float interval_score = interval.GetScore();
                float timing_score = 2.0f * timing.GetScore() / 100.0f;
                float smoothness_score = smoothness.GetScore();
                float vibrato_score = vibrato.GetScore();
                float singup_score = singup.GetScore();

                if (interval_score >= 95.0f) interval_score = 88.0f + 2.0f * (interval_score - 95.0f) / 5.0f;
                else if (interval_score >= 90.0f) interval_score = 87.0f + 1.0f * (interval_score - 90.0f) / 5.0f;
                else if (interval_score >= 85.0f) interval_score = 85.0f + 2.0f * (interval_score - 85.0f) / 5.0f;
                else if (interval_score >= 80.0f) interval_score = 83.0f + 2.0f * (interval_score - 80.0f) / 5.0f;
                else interval_score = 0.0f + 83.0f * (interval_score - 0.0f) / 80.0f;

                if (smoothness_score > 100.0f) smoothness_score = 1.0f;
                else smoothness_score = 1.0f * smoothness_score / 100.0f;

                if (vibrato_score > 100.0f) vibrato_score = 7.0f;
                else vibrato_score = 7.0f * vibrato_score / 100.0f;

                if (singup_score > 100.0f) singup_score = 3.0f;
                else singup_score = 3.0f * singup_score / 100.0f;

                return (interval_score + timing_score + smoothness_score + vibrato_score + singup_score);
            }
            else if (id == 1) return interval.GetScore();
            else if (id == 2) return timing.GetScore();
            else if (id == 3) return smoothness.GetScore();
            else if (id == 4) return vibrato.GetScore();
            else return singup.GetScore();
        }

        // デバッグ用
        public void Debug()
        {
            UnityEngine.Debug.Log("all_time->" + all_time + ", time->" + time);
        }
    }
}

感想

本来の黒よりテクニック評価が厳しいので、なかなかつらい採点になっている。
GUI.DrawTextureで反応テクニックを表示させるのが一番苦労した。
もちろん、こんな採点機はゴミも同然なので、今からオリジナルの採点機を作る。間に合うのかな

6.4 すごいHaskellたのしく学ぼう! 第4章

再帰について。

再帰

再帰(関数)とは、関数内で自身を参照する関数である。
たとえば、フィボナッチ数列再帰関数の代表例である。フィボナッチ数列は以下のように定義されている。
 F(0) = 0,\ F(1) = 1
 F(n) = F(n-1) + F(n-2)\ if\ n \geq 2
2行目の式で、自己参照を行なっているのが分かるだろう?

再帰の利点

再帰で問題を考えることで、部分的に考えたものを全体の答えとすることが出来る。つまり、「どうやって(How)」解くか、ではなく、「どういう(What)」モノか、ということを考えることで、解を得ることが出来る。そもそもHaskellはそういう風に設計された言語なので、再帰と非常に相性が良い。
たとえば、フィボナッチ数列ではF(0) = 0、F(1) = 1と定義されている。これはどうしようもない。このように、最低限与えなければならない情報を基底と呼ぶ。基底さえ考えれば、あとはその関数が「どういうものか?」ということを考えれば良い。

再帰でいろいろ定義する

GHCiでデフォルトで使える関数を自分で定義する。
定義から自分で考えたものがほとんどなので、すごいH本に載っている内容とは異なる場合がある。まぁ、ほとんどの関数は全く同じ実装になっている。

maximum

引数

1つのリスト(ただし、要素は順序付けが可能)。

戻り値

引数リスト内の最大要素。

定義
maximum' :: (Ord a) => [a] -> a
maximum' [] = error "This list is empty."
maximum' [x] = x
maximum' (x:xs) = max x (maximum' xs)
{-
*Main> maximum' [8,2,6,3,1,8,3,9,1]
9
*Main> maximum' [5]
5
*Main> maximum' []
*** Exception: This list is empty.
-}

replicate

引数

整数、要素。

戻り値

要素を整数回繰り返したリスト。

定義
replicate' :: Int -> a -> [a]
replicate' n x
  | n <= 0    = []
  | otherwise = x : replicate' (n-1) x
{-
*Main> replicate' 5 7
[7,7,7,7,7]
*Main> replicate' 0 7
[]
*Main> replicate' (-100) 7
[]
-}

take

引数

整数、リスト。

戻り値

リストから先頭整数個を取り出したリスト。

定義
take' :: Int -> [a] -> [a]  
take' n _
  | n <= 0     = []
take' _ []     = []
take' n (x:xs) = x : take' (n-1) xs
{-
*Main> take' 5 "Hello, World!"
"Hello"
*Main> take' (-8) "Hello, World!"
""
*Main> take' 100000 "Hello, World!"
"Hello, World!"
-}

reverse

引数

リスト。

戻り値

ひっくり返したリスト。

定義
reverse' :: [a] -> [a]
reverse' []     = []
reverse' (x:xs) = reverse' xs ++ [x] 
{-
*Main> reverse' "Hello, World!"
"!dlroW ,olleH"
*Main> reverse' []
[]
-}

repeat

引数

要素。

戻り値

要素の無限リスト。

定義

無限リストなので基底がない。

repeat' :: a -> [a]
repeat' x = x : repeat' x
{-
*Main> take' 10 (repeat' 'H')
"HHHHHHHHHH"
-}

zip

引数

リスト、リスト。

戻り値

同じインデックスの要素をペアとしたリスト。

定義
zip' :: [a] -> [b] -> [(a,b)]
zip' _ []          = []
zip' [] _          = []
zip' (x:xs) (y:ys) = (x, y) : zip' xs ys
{-
*Main> zip' [1,2,3,4,5] ['a','b','c']
[(1,'a'),(2,'b'),(3,'c')]
*Main> zip' [1,2,3,4,5] ['a','b','c','d','e']
[(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')]
*Main> zip' [1,2,3,4,5] ['a','b','c','d','e','f']
[(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')]
*Main> zip' [1,2,3,4,5] []
[]
-}

elem

引数

要素、リスト。

戻り値

要素がリスト内にあるならTrue、ないならFalse。

定義
elem' :: (Eq a) => a -> [a] -> Bool
elem' _ [] = False
elem' n (x:xs)
  | n == x    = True
  | otherwise = elem' n xs 
{-
*Main> elem' 'h' "Hello, World!"
False
*Main> elem' 'r' "Hello, World!"
True
*Main> elem' 'r' ""
False
-}

クイックソート

めっちゃ速いソート。ソートっていうのは、リストを与えられてリスト内のデータを昇順(あるいは降順)に並べ治すこと。Haskellの場合は破壊的代入ができないので、ソートしたリストを返すだけである。
ちなみに、私の頼りない記憶によると、たしか最悪計算量がO(n^2)でバブルソートと等しいが、平均計算量がO(nlogn)でめっちゃ速い、だったと思う。

アルゴリズム

昇順ソートの場合。

  1. 軸とする数字をリスト内から1つ選ぶ(ピボットと呼ぶ)。
  2. ピボット以下のものをピボットの左に、ピボットより大きいものを右にリスト化して置く。これをsmallerOrEqualとlargerと名付ける。
  3. smallerOrEqualとlargerに対して、1から同じ操作を行う。

定義

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
  let smallerOrEqual = [a | a <- xs, a <= x]
      larger = [a | a <- xs, a > x] 
  in  quicksort smallerOrEqual ++ [x] ++ quicksort larger
{-
*Main> quicksort [5,7,2,8,1,6,2,9,4,2,1,0,2,1,7]
[0,1,1,1,2,2,2,2,4,5,6,7,7,8,9]
*Main> quicksort "Hello, World!"
" !,HWdellloor"
*Main> quicksort []
[]
*Main> quicksort [4]
[4]
-}

めっちゃ簡単に実装できた!前期実験でCでクイックソート(動的配列で)実装したけど、割と苦労した覚えがある。

感想

箸休め的な第4章。特に難しい内容はない。
次からは本格的に関数型言語に斬り込んでいく感じ。

6.3 すごいHaskellたのしく学ぼう! 第3章

Haskellの関数構文について。

パターンマッチ

lucky :: Int -> String
lucky 7 = "You are lucky!"
lucky x = "Ku-z."
{-
*Main> lucky 5
"Ku-z."
*Main> lucky 7
"You are lucky!"
-}

関数定義の上から順にパターンマッチが行われ、マッチした場合それが実行される。
パターンに具体的値でなく、小文字から始まる名前を書くと、任意の値に合致するようになる。引数はその名前に束縛される。
もちろん、if/then/elseを使ってもluckyは定義することができるが、見やすさの問題でこちらの方が優れた定義方法と言える。

階乗を計算するfactorialを、再帰とパターンマッチを使って定義する。

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial(n-1)
{-
*Main> factorial 5
120
-}

0の階乗は1と定義する。まず、引数が0であるかチェックする。0ならば1を返す。それ以外の整数ならば、n*factorial(n-1)を返す。再帰について詳しくは第4章でやるので、この辺で置いておく。

パターンマッチは、任意の値を取る場合を考えないとエラーでプログラムがクラッシュする場合があるので、最後に任意値についてのパターンマッチを入れておく方が良い設計である

タプル

addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
{-
*Main> addVectors (3,4) (5,6)
(8.0,10.0)
-}

リスト

*Main> let xs = [(1,1), (2,3), (3,1), (4,3), (5,1), (6,3)]
*Main> [x | (x,1) <- xs]
[1,3,5]

head関数を定義する

head' :: [a] -> a
head' [] = error "list is empty."
head' (x:_) = x
{-
*Main> head' [1,2,3,4,5,6,7]
1
*Main> head' []
*** Exception: list is empty.
-}

asパターン

値をパターンに分解しつつ、パターンマッチ対象となった値自体を参照したい時に使用する。

headAndString :: String -> String
headAndString [] = error "list is empty."
headAndString xs@(x:_) = "head -> " ++ [x] ++ ", string -> " ++ xs
{-
*Main> headAndString "oh,chin chin"
"head -> o, string -> oh,chin chin"
-}

ガード

パターンマッチは具体的な値でしかマッチングが出来なかったが、ガードでは範囲をしていすることでパターンマッチ(みたいなもの)が可能。
BMI指数を入力すると文字列による評価を返すbmiTellを定義する。

bmiTell :: Double -> String
bmiTell bmi
  | bmi <= 18.5 = "You are too thin!"
  | bmi <= 25.0 = "You look good!"
  | bmi <= 30.0 = "You are fat!"
  | otherwise = "You are a whale!"
{-
*Main> bmiTell 21.0
"You look good!"
*Main> bmiTell 32.6
"You are a whale!"
-}

パイプ文字(|)から続く部分がガードと呼ばれる。パイプ文字以降に条件式を書き、Trueならイコール(=)以降の内容が返される。どのガードも通り抜けた場合、otherwiseで受け止められる。

where

ガードを利用して、体重・身長を入力して計算したBMI指数から、文字列による評価を返すbmiTellを(再)定義する。
なお、BMI指数は体重(kg)を身長(m)の2乗で割った値である。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | weight / height ^ 2 <= 18.5 = "You are too thin!"
  | weight / height ^ 2 <= 25.0 = "You look good!"
  | weight / height ^ 2 <= 30.0 = "You are fat!"
  | otherwise = "You are a whale!"
{-
*Main> bmiTell 52.0 1.698
"You are too thin!"
-}

ガリガリやな、って言われた;-)
しかし、この書き方はイマイチであるのは見れば分かると思う。同じ計算を3回繰り返している。
そこで、whereを使うとすっきりと書ける。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | bmi <= 18.5 = "You are too thin!"
  | bmi <= 25.0 = "You look good!"
  | bmi <= 30.0 = "You are fat!"
  | otherwise = "You are a whale!"
  where bmi = weight / height ^ 2
{-
*Main> bmiTell 52.0 1.698
"You are too thin!"
-}

whereのスコープ

whereのスコープは、属するパターンマッチのみである。ガードは1つのパターンマッチの連続と見なされる。
たとえば、以下の関数を考える。

greet :: String -> String
greet "okabi" = nicegreeting ++ ", okabi!"
greet "gojira" = badgreeting ++ ", gojira!"
greet name = normalgreeting ++ "," ++ name ++ "!"
  where nicegreeting = "Hello"
        badgreeting = "Chinko"
        normalgreeting = "Hi"

この関数は正しく動作しない(というかコンパイルエラーになる)。
whereのスコープは1つのパターンマッチのみなので、この関数ではnicegreeting・badgreeting・normalgreetingはgreet nameから始まるパターンでしか認識できないためである。

whereを使って、もう少しスマートにbmiTellを定義する。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | bmi <= thin = "You are too thin!"
  | bmi <= good = "You look good!"
  | bmi <= fat = "You are fat!"
  | otherwise = "You are a whale!"
  where bmi = weight / height ^ 2
        (thin, good, fat) = (18.5, 25.0, 30.0)

where内の関数

where内では関数も定義できる。体重と身長のリストからBMI指数のリストを返すbmiListを定義する。

bmiList :: [(Double, Double)] -> [Double]
bmiList xs = [bmi w h | (w, h) <- xs]
  where bmi weight height = weight / height ^ 2
{-
*Main> bmiList [(52.0, 1.698), (85.0, 1.752), (32.0, 1.519)]
[18.03549107173825,27.69177039678072,13.868657743630061]
-}

let式

whereが後から変数や関数を定義していたのに対し、前から定義するlet式がある。
「式」と名のつく通り、let自体が値を返すという点でwhereと明確に異なる。「式」だから、whereのようにガードをまたぐことはできない。
円柱の表面積を高さと半径から求めるcylinderを定義する。

cylinder :: Double -> Double -> Double
cylinder h r = 
  let sideArea = 2 * pi * r * h
      topArea = pi * r ^ 2
  in  sideArea + 2 * topArea
{-
*Main> cylinder 3 4
175.92918860102841
-}

let式は、let A in Bという形を取る。ここで定義した値はlet式全体でのみ参照できる。
GHCiにおいてinを省略した場合は、全体から参照可能になる。GHCiでlet式を定義した時に、定義した値が返るのは、let式が「式」だからである。

また、let式は「式」なので、リスト内包表記で利用することもできる。
体重と身長のペアのリストから肥満な人のBMIだけをリスト化するbmiList'を定義する。

bmiList' :: [(Double, Double)] -> [Double]
bmiList' xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi > 25.0]
{-
*Main> bmiList' [(52.0, 1.7), (55.0, 1.7), (60.0, 1.7), (70.0, 1.7), (80.0, 1.7), (90.0, 1.7), (100.0, 1.7)]
[27.68166089965398,31.14186851211073,34.602076124567475]
-}

case式

Cとかのswitchでお馴染みのcase。Haskellではコイツも「式」なので、いろいろな使い方ができる。
リストの先頭要素を返すheadを、case式を使って定義する。

myHead :: [a] -> a
myHead xs = case xs of [] -> error "list is empty."
                       (x:_) -> x
{-
*Main> myHead "Chinko"
'C'
-}

case Expression of Pattern1 -> Result1...という書き方になる。of以降のパターンはインデントを揃えないとエラーが起きたので注意。
case式全体が値を返すのでこのような書き方もできる。

describeList :: [a] -> String
describeList xs = "This list is "
                  ++ case xs of [] -> "empty."
                                [x] -> "a singleton list."
                                ls -> "a longer list."
{-
*Main> describeList ""
"This list is empty."
*Main> describeList "c"
"This list is a singleton list."
*Main> describeList "chinchin"
"This list is a longer list."
-}

感想

  • whereは後から定義を補完するもの。
  • let A in B は全体が1つの式となり、その中で利用可能なモノをA内で定義、Bがlet式全体の値となる。GHCiにおいてinを省略した場合は、A自体がlet式全体の値となり、スコープが全体になる。
  • case A of B -> C は全体が1つの式となり、以前定義されているAがBのパターンにマッチする場合Cをcase式全体の値として返す。

式とそうでないものの違いをハッキリ認識することが大切であるようだ。式は必ず値を返す。
だから、ifもif「式」って呼んでたんですね。
パターンマッチの強みは、大きく捉えているもの(たとえばタプルとかリストとか)から部分を取り出して、そこを評価できること。これが出来るのはwhereおよびlet式。caseでは部分評価はできない。
あまり深く考えると頭がおかしくなりそうだから、適当に組んでいくうちにもっと理解が深まるだろう…。というわけで次に進もう。

6.2 すごいHaskellたのしく学ぼう! 第2章

第2章はHaskell型システムについて。

明示的な型宣言

GHCiでは、:tを使うことで型を調べることができる。

Prelude> :t 'a'
'a' :: Char
Prelude> :t True
True :: Bool
Prelude> :t "Hello"
"Hello" :: [Char]
Prelude> :t 4 == 5
4 == 5 :: Bool
Prelude> :t ('a', True)
('a', True) :: (Char, Bool)

Haskellにおける"::"は、「の型を持つ」と読むことができる。
この中で注目すべきは、文字列とタプル。"Hello"はCharのリストとして表現されている。('a', True)は(Char, Bool)という型である。タプルは、要素の数と要素の型の組み合わせで1つの新しい型となる。
Haskellでは、型名はすべて大文字から始まる

関数に明示的な型を与える

removeNonUppercase :: [Char] -> [Char]
removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']]

1行目の表現で、「removeNonUppercaseはCharのリストからCharのリストへの写像である」と読むことができる。
なお、removeNonUppercaseは与えられた文字列から大文字以外を除去した文字列を返す関数である。

引数が2つ以上ある場合は以下のように書けば良い。addThreeは、与えられた3整数をすべて足しあわせて返す関数。

addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z

一般的なHaskellの型

Int

有界整数型。その範囲は処理系依存だが、だいたいは-2^31〜2^31-1なんじゃないかな。
Cとかにおけるint型ですね。

Integer

有界でない整数型。正確にはメモリ上の限界点まで、だろうけど。
引数の階乗を返すfactorialを定義してみる*1

factorial :: Integer -> Integer
factorial n = product [1..n]
-- *Main> factorial 50
-- 30414093201713378043612608166064768844377641568960512000000000000

Float

単精度浮動小数点数*2

Double

倍精度浮動小数点数*3

Bool

真理値型。TrueかFalseしか取れない。

Char

Unicode文字型。文字をシングルクォートで囲む。Charのリストは文字列型。

タプル

タプルの1つ1つが型。一度に最大62個までしか定義できないらしいが、そんなに必要になることはまずないとのこと。空タプルを1つのタプルだとしても合計63個だが、あと1つはなんだろう。

型変数

リストの先頭要素を取り出すheadの型を見てみる。

*Main> :t head
head :: [a] -> a

ここで使われている「a」は小文字から始まっているので型ではない。これは型変数と呼ばれる。C++でいうテンプレートみたいなもの。
型変数名は小文字から始まっていれば何でもいいが、1文字のみで表すことが多い。また、1つの型宣言中に同じ文字が使われている場合、それらの型は同じであることを意味する。

型クラス

…だけど、型変数では少し厄介なことが起こる。
たとえば、第1章でsucc(引数の次の要素を返す)という関数が出てきたが、これに(3,2)というタプルを与えるとどうなるだろうか。「次の要素」が定義されていないので、どうしようもない。
そこで、型変数に制約を与えたい。具体的には、「順番」が存在している型(つまり可算集合)のみを取れるようにしたい。
succの型を見てみよう*4

succ :: Enum a => a -> a

ここで、"::"以降の記述は「aはEnumクラスのインスタンスであり、aからaへの写像である」と読める。Enumクラスは性質として順番が定義できる。
型を統合させるものが型クラスである。すなわち、以下のような構造をしている。

Haskellの型クラス

Eq

等値性を持つクラス。==および/=による比較が可能。

Ord

大小比較ができるクラス。>および<による比較が可能。なお、その性質上Eqクラスに部分集合として含まれる*5

Show

文字列に変換可能なクラス。たとえば、

*Main> show 1234
"1234"

Read

文字列から変換可能なクラス。たとえば、

*Main> read "True" || False
True

Enum

順番があるクラス。

Bounded

有界なクラス。

Num

数を表すクラス。加減乗除が定義可能。

Floating

浮動小数点数を表すクラス。

Integral

整数を表すクラス。

型推論

ここで、fromIntegralという関数の型を調べる。

*Main> :t fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b

すなわち、与えた整数を何らかの数型に変換する関数である。
たとえば、以下のような状況で使用する。

*Main> length [1,2,3,4] + 1.0
# Error。length :: [a] -> Int なので、IntとFloatの足し算になるため。
*Main> fromIntegral( length [1,2,3,4] ) + 1.0
5.0  # FloatはNumのインスタンスなのでOK。

…と、このようにfromIntegralが返す型は明示的に宣言されていないが、Haskellは状況に応じて必要な型を推論し、与えられたクラスから適当なインスタンスを決定する。これを型推論という。
Haskellは非常に強い静的型付けを持ちつつ、強力な型推論も備えている。
型が一意に定まらない場合、Haskellはエラーを返す。たとえば、以下のような例を考えると良い。

*Main> read "5" - 2
3  # 2がIntなので、read "5" はIntとして一意に定まる。
*Main> read "5"
# エラー。Numのインスタンスが一意に定められない。

感想

型・型クラス辺りは文章で読んでもピンと来ないので、集合論的に考えるとスッキリする。

*1:普通は再帰で書くだろうけど、再帰によるfactorialは第4章で出てくるので、ここはリストのproductを利用する。

*2:たんせいどふどうしょうすうてんすう。良いタイピング練習になった。

*3:ばいせいどふどうしょうすうてんすう。良いタイピング練習になった。

*4:なお、演算子の型をチェックするには丸括弧()で囲む必要がある。たとえば、「:t (==)」。

*5:「aに等値性を与えられること」と「aに大小を与えられること」って同値なんですかね…。同値だとしたら分けてる意味ないし、例外あるんだろうけど。教えてエライ人!