14
4

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

RustAdvent Calendar 2019

Day 13

Rustで自然変換する話

Last updated at Posted at 2019-12-13

はじめに

さて前回まで、関数型言語を使うプログラマーにとって「ある構成がただの関数ではなく、実は本質的には自然変換である」ケースに気づけることが、関数型言語をより良く使いこなす上で重要になってくるのだという話をしてきました。

えっ、してない?

今日は13日の金曜日だ…奇跡がおきても不思議じゃない…

そんな話です。よろしくお願いいたします。よろしくお願いいたしました。

関手と自然変換

関手 is 何?

まず VecOption など、ある種のものが「関手」と呼ばれるものであると考えられることから見ていきましょう。まずこれらは一旦「型 T を受け取って、新しい型 Vec<T> を返す」関数と考えます。

このとき Vec が「型 S から 型 T への矢印」を「型 Vec<S> から型 Vec<T> への矢印」に変換する働きを持っている場合、こいつが関手であると雑に考えることにしましょう。矢印というのはお察しの通り、この場合はただの関数です。

let a: Vec<String> = vec![1,2,3,4,5].iter().map(|i| i.to_string()).collect();

to_string という「i32 から String への矢印」が、全体として「Vec<i32> から Vec<String> への矢印」に「変わっている」ことに注意しましょう。Option も大体同じです。

自然変換

以下のような「関数」を考えてみます。

fn vec_to_option_nt<T: Clone>(src: &Vec<T>) -> Option<T> {
  src.get(0).map(|x| x.clone())
}

Vec の先頭要素をあれば返す(なければ None)だけの簡単な関数です。

fn main() {
  let a = vec_to_option_nt(&vec![1, 2, 3, 4, 5]);
  println!("{}", a.map(|i| i.to_string()).unwrap_or("None".to_string()));

  let a = vec_to_option_nt(&vec!["red", "blue", "green", "yellow", "purple", "red-no-ikiwakareta-ani-gold"]);
  println!("{}", a.map(|i| i.to_string()).unwrap_or("None".to_string()));

  let a = vec_to_option_nt(&(vec![] as Vec<f32>));
  println!("{}", a.map(|i| i.to_string()).unwrap_or("None".to_string()));
}

この関数は型パラメータ T を通じて

  • Vec<i32> -> Option<i32>
  • Vec<char> -> Option<char>
  • Vec<bool> -> Option<bool>
  • Vec<String> -> Option<String>

...

  • Vec<OregaKangaetaSaikyouNoStruct> -> Option<OregaKangaetaSaikyouNoStruct>

への変換を全部同時に引き起こしていることがわかります。つまり「関手 Vec を関手 Option」に変換していると考えて良いということになります。これが自然変換です。

何だただのジェネリックな関数か...と思われるかもしれません。たしかに一面ではそうですが、「自然変換」であるためには単にジェネリックであるだけでは足りません。

自然変換は、任意の「型 S から 型 T への矢印」(つまり関数)と交換可能であるという性質を持っています。(今回は雑に考えているので、任意と書きましたが、上記の実装では Clone なものに限られます。)

意味がわかりませんね。わからないので、以下のコードを見てみましょう。

use std::time::Instant;

fn vec_to_option_nt<T: Clone>(src: &Vec<T>) -> Option<T> {
  src.get(0).map(|x| x.clone())
}

fn main() {
  let target = (0..10000000).collect::<Vec<i32>>();
  let int_to_bool_fn = |i: i32| -> bool { i % 2 == 0 };

  let time = Instant::now();
  let result1 = vec_to_option_nt(&target).map(int_to_bool_fn);
  println!("{}", time.elapsed().as_millis());

  let time = Instant::now();
  let result2 = vec_to_option_nt(&target.clone().into_iter().map(int_to_bool_fn).collect());
  println!("{}", time.elapsed().as_millis());

  assert_eq!(result1, result2);
}

vec_to_option_nt をしてから int_to_bool_fn をしても、int_to_bool_fn をしてから vec_to_option_nt をしても結果が変わっていないことがわかると思います。

結果が同じなので、多分実装はどちらでも良いんじゃないかなと思います。多分ですが。

この性質は「関数がジェネリックに書ける」ことだけではどう頑張っても導出されないので、「自然変換になっているか」はプログラマーが明確に意識する必要がありそうです。

また、ある構成が本質的には自然変換であることに気づけば、既存の自然変換を「合成」することで(普通の意味で)自然で、スマートな実装が得られるかもしれません。もちろんこの場合、ジェネリックなのはおまけでしかなく、自然変換であること is 本質。

さらに「これ…関数というよりは、本質的には自然変換じゃないですかねぇ…(ドヤァ)」とドヤ顔できますね。

おわりに

いかがでしたか?

関数型言語を使うプログラマーにとって「ある構成がただの関数ではなく、実は本質的には自然変換である」ケースに気づけることが、関数型言語をより良く使いこなす上で重要になってくるのだという話をしてきました。

もともと Scala で書いていたのですが Rust で書き直してみたところ、とても大変なことになりました。

「Rustは学習コストが高い」そう考えている人も多いかと思います。ですが、そういった人たちにも「やっぱりRustは学習コストが高い」ということが伝われば幸いです。

14
4
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
14
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?