なっとく関数型プログラミング : Rustで関数型プログラミング #1

なっとく関数型プログラミングを読み始めました。

Rustを書き始めてから、関数型プログラミングが気になりだし、関数型プログラミングを書けるようになりたい!

関数型プログラミングかける人ってなんか超カッコ良くない?!って憧れるようになりました。

しかし、関数型プログラミングの本は難解なものも多く、何度か足を踏み入れようとしては挫折... を繰り返していました。

そんな私の事情を知っているのか、Amazonはログインするたびにおすすめの商品で本書を推してきて、評価も良かったのでポチってやりました。

本書ではJavaScalaを使って命令型と宣言型の例を交えて、わかりやすく関数型プログラミングを説明されています。

手順を丁寧に踏んで説明されているので、こんな私でも挫折することなく読み進めることができています。

Scalaでのサンプル確認に加え、Rustならどう書ける?っていうのを考えながら読んでます。

関数型プログラミングって何?

まずは、関数型プログラミングって何でしょう?簡単に言うと、プログラムを「関数」の組み合わせで考えるスタイルのことです。

  • 関数: 数学の関数みたいに、特定の入力に対して特定の出力を返します。副作用がない関数を純粋関数といいます。
  • 不変性: 変数の状態を変えないことです。
  • 高階関数: 関数を引数として受け取ったり、関数を返すことができる関数です。

4章まで読んで気になったワード

データのコピーを渡す

関数型プログラミングではデータを直接変更するのではなくコピーを返す。 データのコピーを渡すことは関数型プログラミングで行う基本的なことの一つ。*1

嘘をつかない関数。関数のシグネチャは事実をありのままに伝えるべきである。

関数のシグネチャは関数の振る舞いそのままを伝えるべきで、内部でそれ以外の機能を詰め込むとコードが読みにくくなり、テストも困難で、管理しにくくなる。

アルゴリズムを引数として渡す。

関数のシグネチャをみるだけで、関数内で行われることを推測できる必要がある。 *2

カリー化

複数のパラメータを持つ関数を、関数から関数が返される一連のパラメータを1つの関数に変換すること。

Rustでの例

use core::fmt::Debug;

pub fn testexe() {
    let ns1 = [5, 1, 2, 4, 0];
    let ns2 = [5, 1, 2, 4, 15];
    let mst1 = ["scala", "ada"];
    let mst2 = ["rust", "ada"];

    printfill(&ns1, curry_bool(larger_than, 3)); // -> [5,4]
    printfill(&ns2, curry_bool(divisible_by, 5)); // -> [5,15]
    printfill(&mst1, curry_bool(shorter_n, 4)); // -> ["scala"]
    printfill(&mst2, curry_bool(number_of_s_larger_n, 1)); // -> ["rust"]
}

// 配列にFillterをかけVecをprintするhelper関数
fn printfill<T: Debug + Copy>(ns: &[T], scorer: impl Fn(T) -> bool) {
    println!(
        "{:?}",
        ns.iter().filter(|i| scorer(**i)).collect::<Vec<_>>()
    )
}

// カリー化
fn curry_bool<T, N: Clone>(myfunc: impl Fn(T, N) -> bool, n: N) -> impl Fn(T) -> bool {
    move |my_n| myfunc(my_n, n.clone())
}


fn divisible_by(i: i32, n: i32) -> bool {
    i % n == 0
}

fn shorter_n(st: &str, n: usize) -> bool {
    st.len() > n
}

fn number_of_s_larger_n(st: &str, n: usize) -> bool {
    st.chars().filter(|x| *x == 's').count() >= n
}

fn larger_than(i: i32, n: i32) -> bool {
    n < i
}

Rustでの関数型プログラミングの応用  *3

クロージャ高階関数イテレータを用いた例

Rustでは、クロージャを使って関数を定義したり、高階関数を利用して他の関数を引数に取ることができます。 以下は本書のサンプルコードをRustで実装した簡単な例です。

pub fn testexe() {
    let numlist = [5, 1, 2, 4, 100];
    // 合計
    println!("{:?}", numsum(&numlist));
    // 文字列の長さの合計
    println!(
        "{:?}",
        strsum(&["scala", "rust", "ada"], |x| x.len() as i32)
    );
    // sの個数の合計
    println!(
        "{:?}",
        strsum(&["scala", "haskell", "rust", "ada"], |x| scount(x, 's'))
    );
    // 最大の数値
    println!("{:?}", [5, 1, 2, 4, 15].iter().fold(0, |a, b| a.max(*b)));
}

fn numsum(nums: &[i32]) -> i32 {
 //数列の合計を返す
    nums.iter().fold(0, |a, b| a + b)
}

fn strsum(strs: &[&str], score: impl Fn(&str) -> i32) -> i32 {
 //文字の配列をscore(関数)に基づいた値で集計し、合計を返す
    strs.iter().fold(0, |a, b| a + score(b))
}

fn scount(st: &str, ch: char) -> i32 {
 //st(文字列)に含まれるch:charと同じ文字の個数を返す
    st.chars().filter(|x| x == &ch).count() as i32
}

4章まで読んで

読み始めた当初は不変性や純粋関数に厳格で、配列のコピーのコストを気にしないなど戸惑いを感じていました。 読み進めるにつれ、「ああ、こういう事か」と腑に落ちてきて関数型プログラミングの考え方に馴染んできました。

Rustを書く時も、イテレータのfilterやmapの使い方が変わってきました。 できるだけforは避けたいと思うようになり、イレテータを使用することが多くなってきました。

続きは本を読み進めながら記事にしていく予定です。

なっとく関数型プログラミング

*1:コピーするとパフォーマンスが低下するのでは?って引っかかりますよね。本書では多くのケースにおいてコードベースの読みやすさと管理しやすさが潜在的なパフォーマンスの低下をはるかに上回ると説明されています

*2:当然のことですが、関数自体の名前も関数内で行われることを推測できる必要がある。昔の自分コードを見返すとそうなっていない部分もあって激しく反省しています。

*3:本書ではRustのコードの説明はありません。 サンプルコードはscalajavaで書かれています