Help us understand the problem. What is going on with this article?

HaskellerがRustに入門してみた

こんにちは。Homarechanというものです。

最近よく聞くRust、気にはなっていたのですが課題など1が重なり後回しになっていました。
やっと入門します。

なお、題名からわかるようにこの記事はHaskellerがRustに入門してみたなので、Rustの詳しい解説をするものではありません。記事中にHaskellコードがいっぱいでてくる。かも。

※筆者のRust/Haskell力が圧倒的に足りず、多くの方に訂正は頂いているのですが説明が想像以上にガバガバになっています(特に型)。厳密な説明を求める方はRustのドキュメントや他の方の記事を参照してください。訂正していただいた方々ありがとうございました。

※2 本当に想像以上に間違いが多く一旦消すかどうかも悩みましたが、改悛・懺悔の念も込めまして残しておきたいと思います。

Let's go

RustはMozillaが支援するオープンソースのシステムプログラミング言語である。
Wikipediaより

ふむふむ、、、システムプログラミング言語なんですね。
つまり、CやC++、PL/Sなんかと同じ位置づけになるのかな?

Haskellはランタイムが大きくシステムプログラミングには向いていないので、既に相違点が。

基本構文

変数

Rustの変数はletキーワードで宣言します。MLみたいですね。

Rust
let a = 123;       // 推論が効きます。
let b: i32 = 100;  // もちろん、教えてあげることもできます。
Haskell
a = 123         -- もちろんHaskellも推論してくれます。
b = 100 :: Int  -- 教えてあげることも可能。

Rustの変数は標準でイミュータブルで、書き換えはできません。Haskellと同じですね。

ただし、Haskellと違いミュータブルな変数を定義することも可能です。

Rust
let mut c = 123;  // mutキーワードで宣言してやると…
c = 456;          // 書き換えても怒られない!

便利ですね〜
次!

関数

Haskellといえば関数型プログラミング。関数型プログラミングといえば関数。
実はRustも関数型プログラミングに向いた言語( *要出典)( *個人的見解)なのです!
ではまず引数を二倍して返す関数twiceを定義します。
Haskellについてはポイントフリーなど難しいことは考えず愚直に引数を取って定義します。

Rust
fn twice(n: i32) -> i32 {
  // 関数の型は省略できない!
  n * 2
}
Haskell
twice :: Int -> Int -- 関数の型も推論してくれるので省略可能!
twice n = n * 2

なんだかぜんぜん違うような少し似てるような感じですね。

まず、Rustの関数はfnキーワードで定義します。命名法はsnake-caseです。スネークケースじゃないと警告してくれます。

Rust
fn camelCaseFunction() {
  do_something();
}
Shell
$ rustc camel.rs
warning: function `camelCaseFunction` should have a snake case name
 --> camel.rs:1:4
  |
1 | fn camelCaseFunction() {
  |    ^^^^^^^^^^^^^^^^^ help: convert the identifier to snake case: `camel_case_function`
  |
  = note: `#[warn(non_snake_case)]` on by default

warning: 1 warning emitted

神!

Haskellの場合、関数名はキャメルケースが一般的ですが、処理系が警告、まではしてくれませんのでその点はRustの方が優しいと言えますね。

また、Rustの関数の引数、戻り値は型推論が効きません。技術的には可能なのですが再利用性や可読性を考慮して関数の型は明示が必須とされています。Haskellも推論効くけどどうせみんな書いてるでしょ?じゃあ黙って書こう。ってことです。

そして、Rustの関数では最後に実行された式の値がそのまま関数の戻り値になりますので、最後の行にreturnは不要です。returnキーワードはあるのですが、いわゆる早期リターンに使われる構文で、必要のない場所で使うのはよろしくないとされています。

構文じゃないけど一応ここで触れておきます。

Rustにはプリミティブな型が結構大量にあります。Haskellと対応付けて列挙していきます。

Rust Haskell
bool Bool
i8/u8 Int8/Word8
i16/u16 Int16/Word16
i32/u32 Int32/Word32
i64/u64 Int64/Word64
i128/u128 WideWord.Int128/WideWord.Word128
isize/usize Int/Word
f32/f64 Float/Double
[T; N]/[T] [a] (*)
(T, U) (a, b)
str String (*)

*意味的な分け方をしているので、正確ではありません。

上から説明していきましょう。

まずbool。言わずもがな論理値ですね。サイズは1ビットです。

※5/12訂正
Rustのboolは1バイトでした
@YoshiTheChinchilla さんありがとうございます

次にi8/u8i128/u128。Rustでは標準で128bitの大きさまでの整数型をサポートしています。Haskellは64bitまでなので、Integerを除けばそれよりも大きい値を扱えることになります。

そしてisize/usize。C言語などを使ったことがある方はsize_tと言ったらわかりやすいかもしれません。Cのsize_tは符号なし整数ですが、Rustでは符号ありとなしの両方が用意されており、isizeが符号あり、usizeがなしです。システムプログラミング特有の型と言えるので、Haskellにはないですね。

※5/11訂正
Haskellにおけるアーキテクチャ依存の整数型はIntWordでした。うっかりしていました…
@igrep さんありがとうございました。

f32/f64はそのまま、単精度浮動小数点数と倍精度浮動小数点数です。前者がHaskellで言うFloat、後者がDoubleとなります。

[T; N]。見慣れない感じですが、これはいわゆるジェネリックです。

中にTという型が入っていて長さがNの配列を表します。HaskellのListと並べましたが、Rustの[T; N]は実際はリストではないので正確ではありません。リストは標準ライブラリにLinkedList<T>という型で登場します。

[T]は、[T; N]のとある範囲に対して高速にアクセスするためのスライスと呼ばれる型です。
[T; N]の長さがコンパイル時に決定されるのに対し、スライスの長さは実行時に決まるので、型に長さの情報が含まれません。
専ら&[T]のように借用として扱われ、[T; N]のみならず、同じ型がメモリ上に連続して存在するようなデータなら何でもスライスにすることができます。

(T, U)は、お察しの通りタプルです。Haskellの(a, b)と同じく、配列/リストと違い中身の型はバラバラでもOKです。また、長さは二つとは限らず3個でも100個でもメモリが許す限り入れられます。

最後にstrですが、Rustはシステムプログラミング言語という特性上、文字列の扱いがHaskellよりも少々難しくなっています。と言ってもC程ではないので安心してください。

strは、文字列スライスと呼ばれ、Rustにおいて最もプリミティブな文字列型です。標準ライブラリにはStringという別の型が用意されているのですが、余分な情報が含まれるためstrに比べると効率があまり良くありません
※5/15訂正
Stringは避けられるなら避けたほうが良い、というのは効率のせいではなくメモリのアロケートがヒープ領域に対して行われるためでした。
@osanshouo さん、ありがとうございました。

strはコンパイルされたプログラムの中にハードコードされた文字列を表す型なので、専ら&strという形で、その文字列のメモリ上の位置として表されます。これは借用というRustのメモリ安全戦略で最も重要とも言える仕組みです。

※5/12訂正
strStringのスライスなので、ハードコードされた文字列に限らないです。
@mosh さんありがとうございました

次!!!

データ型

Rustではstructenumという2つのキーワードでデータ型を定義します。早速見てみましょう。

Rust
struct Human {
  age: u32,
  name: String,
  gender: Gender,
}

enum Gender {
  Male,
  Female,
}
Haskell
data Human = Human { age :: Int
                   , name :: String
                   , gender :: Gender}

data Gender = Male | Female

Structは構造体、enumは列挙体と呼ばれるものです。

Haskellではどちらもdataで定義しますが、Rustでは予約語が別れています。

実はこのenum代数的データ型といい、Haskellのdataと同様の表現力を持っています。

Haskell
data Number = One | Two | Three            -- 列挙型

data Point = Point Int Int                 -- 直積型

data SmokingInfo = NonSmoker | Smoker Int  -- 直和型
Rust
// 列挙型
enum Number {
  One,
  Two,
  Three,
}

// 直積型
enum Point {
  Point(i32, i32),
}

// 直和型
enum SmokingInfo {
  NonSmoker,
  Smoker(u32),
}

直和型、思いつかなかったからって無理があるでしょ

という意見は無視しつつ、Rustのenumで表せる3つの形の例を出してみました。

実際には直積型にはstructを使うのでこのような使い方はしないのですが、一応可能だということを示しておきます。

列挙型

その名の通り、値を列挙しています。

直積型

複数の値の積を表現するのでこのような名前になっています。structで書くとこんな感じ

Rust
struct Point {
  x: i32,
  y: i32,
}

こちらのほうがフィールドに名前がついていたりして扱いやすい気がしますね。

※5/11訂正

直積の各要素にも名前が付けられらことを知りませんでした…
@kazatsuyu さん、ありがとうございます

Rust
enum Point {
  Point {
    x: i32,
    y: i32,
  },
}
直和型

複数の直積型の和を再現します。今回は、非喫煙者、喫煙者の列挙で、喫煙者には喫煙歴のフィールドをもたせています。structを合わせるとこうなります。

※5/11訂正
Smokerの定義を間違えていました
@kazatsuyu さん、ありがとうございます。。。

Rust
struct Smoker {
  SmokingHistory: u32
}

enum SmokingInfo {
  NonSmoker,
  Smoker(Smoker),
}

ということで、次!!

パターンマッチング

Rustの特徴の一つと言えるパターンマッチング。関数型プログラミングでは定石となっているのでサラッと行きます。

まずはmatchキーワードでのシンプルなパターンマッチング

Rust
enum Signal {
  Blue,
  Yellow,
  Red,
}

fn signal_to_string(signal: Signal) -> String {
  match signal {
    Signal::Blue => "Go!",
    Signal::Yellow => "Hmmmmm, GO!",
    Signal::Red => "Stop!!!!",
  }
}
Haskell
data Signal = Blue | Yellow | Red

signalToString :: Signal -> String
signalToString Blue = "Go!"
signalToString Yellow = "Hmmmmm, GO!"
signalToString Red = "Stop!!!!"

-- case文でも
signalToString :: Signal -> String
signalToString signal = case signal of
                          Blue -> "Go!"
                          Yellow -> "Hmmmmm, GO!"
                          Red -> "Stop!!!!"

Haskellとなんら変わりがないように見えます。しかし、RustのパターンマッチングはHaskellよりもチェックしてくれる項目が多いです。例えばこのコード、黄色の時の処理を書き忘れていたとします。

Haskell
data Signal = Blue | Yellow | Red

signalToString :: Signal -> String
signalToString Blue = "Go!"
signalToString Red = "Stop!!!!"

これは、コンパイルが通ってしまいます。そしてこの関数にYellowを渡してしまうと…

Haskell
main = putStrLn . signalToString $ Yellow
Shell
$ ghc main.hs
$ ./main
Main: Main.hs:(5,1)-(6,31): Non-exhaustive patterns in function signalToString

実行時エラーが出てしまいます。チーン。

しかしこのエラー、Rustなら防げます。2

Rust
enum Signal {
  Blue,
  Yellow,
  Red,
}

fn signal_to_string(signal: Signal) -> String {
  match signal {
    Signal::Blue => "Go!",
    Signal::Red => "Stop!!!!",
  }
}

fn main() {
}
Shell
$ rustc main.rs
error[E0004]: non-exhaustive patterns: `Yellow` not covered
 --> main.rs:8:9
  |
1 | / enum Signal {
2 | |   Blue,
3 | |   Yellow,
  | |   ------ not covered
4 | |   Red,
5 | | }
  | |_- `Signal` defined here
...
8 |     match signal {
  |           ^^^^^^ pattern `Yellow` not covered
  |
  = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms

error: aborting due to previous error

For more information about this error, try `rustc --explain E0004`.

ちゃんと弾いてくれるどころか、丁寧にドキュメントまで提示してくれます!何だこの素晴らしい処理系は…

パターンマッチングはまだまだ他の使い方もあります。

Rust
let (first, second) = (1, 2);
println!("{}, {}", first, second) // 1, 2
Haskell
let (first, second) = (1, 2)
putStrLn $ show first ++ ", " ++ show second

そっくりですね。このように、パターンマッチングでフィールドの分解も出来ちゃいます。

次!!!!!!!!

trait, impl

ここから更にHaskellとの共通点が浮き彫りになってきます。

trait

先程少しだけ登場したジェネリックをここでもう少し詳しく見てみたいと思います。

まず、引数をそのまま返す使い所のわからない関数を定義したいと思います。引数の型は一意に決めたくないので、Tという名前でジェネリクスにします。

Rust
fn ret<T>(x: T) -> T {
  x
}

ここでのTは任意の型で、コンパイル時に各呼び出し元で決定されます。

Rust
ret(1);     // Tはi32
ret(true);  // Tはbool
ret([1,2]); // Tは[i32; 2]

もちろんこれはコンパイルが通ります。

次に、引数を標準出力に出力してから返すこれまた使い所のわからない関数を定義します。

Rust
fn print_and_ret<T>(x: T) -> T {
  println!("{}", x);
  x
}

お気づきになっている方もいるかも知れませんが、これはコンパイルが通りません。なぜでしょう?

答えは簡単で、Tが標準出力に出力できる保証がないからです。

Shell
$ rustc main.rs
error[E0277]: `T` doesn't implement `std::fmt::Display`
 --> main.rs:2:18
  |
2 |   println!("{}", x);
  |                  ^ `T` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `T`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Display::fmt`
  = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider restricting type parameter `T`
  |
1 | fn p<T: std::fmt::Display>(x: T) -> T {
  |       ^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.

Haskellで言うところのShow型クラスをサポートしていないので文字列に変換できない感じです。

Haskellで、与えられた値を出力する関数を定義するならばこう書くはずです。

Haskell
print' :: Show a => a -> IO ()
print' x = print x

そのまますぎて悲しくなります。

そうです、引数の型をShow型クラスをサポートしている型に限定してやれば良いのです。
Rustでも同じことが可能です。

Rustでは、Haskellの型クラスにあたる概念をトレイトとして提供しています。そして、トレイトによって限定された関数の引数の型の範囲をトレイト境界といいます。
つまり、print_and_ret関数にトレイト境界を設定してやればコンパイルが通るはずです。そして、Rustにおいて画面出力をサポートするトレイトはstd::fmt::Displayといいます。

Rust
fn print_and_ret<T: std::fmt::Display>(x: T) -> T {
  println!("{}", x);
  x
}

fn main() {
  let x = print_and_ret(123);
  println!("{}", x);
}

これでちゃんとコンパイルが通ります!

impl

Haskellのclasstraitならinstanceは?

implです。ただ、RustのimplにはHaskellのinstance以上の機能があります。一つずつ見ていきます。

文字列に変換できることをサポートするShowトレイトをRustで実装してみましょう。

Rust
trait Show {
  fn show(&self) -> String;
}

まず、トレイトの関数テンプレートのように中身がない関数を定義するときはこのようにセミコロンで終わらせます。
引数の&selfですが、自分自身を借用している、という意味になります。(ここまで来てなんだけどこの記事では恐らく所有権については解説しません><)

そして、これを自作の構造体に対して実装してみます。

Rust
struct Point {
  x: i32,
  y: i32,
}

impl Show for Point {
  fn show(&self) -> String {
    format!("({}, {})", x, y)
  }
}

format!マクロは、第一引数のフォーマットに従ってそれ移行の引数を文字列に変換するマクロです。(所有権と同じく、マクロについても解説しません><)

これはHaskellと同じような使い方です。ではHaskellと違う使い方もしてみましょう。

implは、構造体や列挙体に対してメソッドを定義する時に使います。先程のPoint構造体を少し便利にしてみましょう。

Rust
impl Point {
  fn new(x: i32, y: i32) -> Self {
    Self {
      x,
      y,
    }
  }

  fn get_x(&self) -> i32 {
    self.x
  }

  fn get_y(&self) -> i32 {
    self.y
  }

  fn set_x(&mut self, x: i32) {
    self.x = x;
  }

  fn set_y(&mut self, y: i32) {
    self.y = y;
  }
}

5つの機能を追加してみました。

1つ目、newは、引数にselfがありません。このようなメソッドを関連関数といいます。
2,3つ目は自分自身の参照を受け取り、メンバ変数の値を返すものです。
3,4つ目は自分自身のミュータブルな参照を受け取り、メンバ変数の値を変更するものです。値の更新を行うのでミュータブルな参照を必要とします。

これらのメソッドは

Rust
// 後に変更するのでmutで宣言する
let mut p = Point::new(1, 2);
println!("{}", p.get_x()); // 1
println!("{}", p.get_y()); // 2

p.set_x(5);
p.set_y(10);

println!("{}", p.get_x()); // 5
println!("{}", p.get_y()); // 10

このようにして呼び出すことができます。一気にオブジェクト指向感が溢れてきましたね。

これで基本的な構文、機能は一通り洗えたので、これからRustとHaskellの似ている点を中心にもう少し紹介していこうと思います。

失敗の再現

Option

Haskellで代表的な失敗を表すモナドといえばMaybe aではないでしょうか?
実はRustにもそれに対応する型が存在します。

Option<T>という名前で、このように実装されています。

Rust
pub enum Option<T> {
    None,
    Some(T),
}

Haskellで言うNothingNoneJust aSome(T)です。

このようにして処理できます

Rust
let o: Option<i32> = Some(123);

match o {
  Some(x) => println!("Success: {}", x),
  None => println!("Error"),
}

またはif-let文という構文を用いてこのように簡潔に表すことも可能です

Rust
let o: Option<i32> = None;

if let Some(x) = o {
  println!("Success: {}", x);
} else {
  println!("Error");
}

実際にはこちらの書き方が推奨されています。

このOption<T>型にもいろいろなメソッドが提供されており、その多くはHaskellでもData.Maybeの中で同様の関数が提供されています。

Result

Haskellの入門書でMaybe aの次に出てくるモナド、Either a bです。これに関しても同様の型が用意されています。

Rust
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

let r: Result<i32, String> = Ok(123);

match r {
    Ok(x) => println!("Sucess: {}", x),
  Err(e) => println!("Error: {}", e),
}

これにもOption<T>と同じく多くのメソッドが提供されています。

RustにはHaskellのerrorに似たpanic!というマクロが存在しますが、Haskellと同じように呼び出し元で適切にエラーハンドリングをして対処することが推奨されているので、あまり使うことはありません。

途中ですが、長くなってきたので記事を分けたい感と眠気に負けたのでとりあえずここまでで公開します。間違い、誤字脱字などありましたらコメント、編集リクエストお願いいたします。
また気が向いたら追加していきたいと思います。

このように、Haskellの影響を受けまくっているRustさんはHaskellとの共通点がとても多く、Haskellerが新しく学ぶにはもってこいの言語なのでした(続くかも)


  1. 春季課題はまだ終わってません 

  2. 実を言うと弾いてくれない言語の方が珍しかったりするのですが… 

maki_0419lo
関数型ばっかり書いてる学生です
https://twitter.com/maki_0419lo
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした