関数型プログラマのためのRust

この投稿はEdward Z. Yangが2010年に書いたOCaml for Haskellers、私自身が今年頭に書いたHaskell for OCaml programmersの流れに沿っています。

目次


: この記事の最新版は下記サイトで見られます。
http://science.raphael.poss.name/rust-for-functional-programmers.html
他のフォーマット:Source, PDF

プロローグ

C言語プログラマのためのRustと、関数型プログラマのためのRustは異なります。

他のプログラミング言語を使用した経験のあるC言語系統のプログラマなら、Rustはとても多くの新しい特徴と変わった構造を持っていると感じるでしょう。どちらかというと難解と感じるかもしれません。C++、JavaやC#プログラマでさえ、Rustは別物のように感じます。Rustはオブジェクトを持ちますがクラスを持ちません。“structed enums”を持ち、C言語系統では同等のものがない変わったマッチステートメントを使います。同じ変数を2度割り当てること避け、C系統言語では聞いたことのない変則的なルールを適用しています。

それはなぜでしょう?

現在のRustマニュアルではあえて明言していないものの、Rustは関数型言語であり、ここ数年主流となっているプログラミング言語デザインの影響を受けています。

Rust支持者が直面している問題があります。それは言語としても規律としても、関数型プログラミングが30年にわたり汚名を着せられてきたことです。例えば、関数型プログラムは判別が難しい、C言語に比べ処理が遅い、実行時には高くつく機構が必要になる(少なくともガベージコレクタ、しばしばシステムI/Oの関数型ラッパなど)、また学ぶのが難しく、変則的なシンタックスを使用するなどです。

C言語プログラマへ関数型言語としてのRustを説明する場合、受け入れてもらうにはとても骨が折れるでしょう。というのも公式のRustマニュアルとチュートリアルは明確に、下位タスクの実行に、どのようにRustを使用するか説明していますし、多くのCプログラミングパターンに相当するRustを現行のプログラミング言語に言及することなく、少しずつ説明していくとしています。

これがすべてであると言えるのですが、もしあなたがすでに関数型プログラミングについて知っていたらどうでしょうか?

Haskell、OCamlなどを使用したことのある経験者にとって、関数型プログラミングの基礎を事細かく説明したマニュアルを読むことはとても退屈なことでしょう。対象者のレベルに合わせ、以下では関数的側面から急ぎ足でRustの紹介をしていきます。

なぜRustを学ぶべきなのか

良くも悪くも、この先も大抵のハードウェアプロセッサはプログラムカウンタ、レジスタ、そしてアドレス指定のできるメモリに基づいていくだろうとされています。これは私たちが1970年代にいた場所とまったく変わっていません。当時はC言語が設計された時代であり、C言語が現在も普及している理由になっています。特にC言語の抽象機械モデルはほとんどのハードウェアのプラットフォーム上で適しているとされ、割り込みハンドラやガベージコレクタ、仮想メモリマネージャのような低レベルのシステムを構築するのにちょうどいいレベルの抽象化になっています。

しかしながらC言語の“ユーザインターフェース”、特にプリプロセッサと型システムは、とても時代遅れになっています。新しいものを学んだプログラマにとっては、率直に言ってダメな言語です。

こうしたことを背景にRustはつくられてきました。RustはC言語の抽象機械モデルを保ちますが、言語のインターフェースが刷新されています。Rustは書き方のバリエーションが豊富で、型システムはシステムコードをより安全にします。その強力なメタプログラミングファシリティはコードの自動作成の新しい方法を可能にします。

しかし、この記事を書いている時点ではRustは、いまだ安定していないことに注意してください。ほとんど予告なく、まだ変更されることがあり、公式の資料は実装と完全には同期されていません。

直接対応が可能なもの

構文 Type Ocaml Haskell
() () () ()
true bool true True
false bool false False
123 (integer)   123 {- :: Num a => a }
0x123 (integer)   0x123 {- :: Num a => a }
12.3 (float)   12.3 {- :: Fractional a => a }
'a' char   'a'
"abc" str "abc" "abc"
b'a' u8 'a' toEnum$fromEnum$'a'::Word8
123i int 123 123 :: Int
123i32 i32 123l 123 :: Int32
123i64 i64 123L 123 :: Int64
123u uint   123 :: Word

Haskellと同じように、Rustの数値リテラルでサフィックスがないものについでは、あらかじめ定義された型はありません。実際の型はコンテキストから推論されます。Rustの文字リテラルはどんなUnicodeのスカラ値も表すことができ、Latin-1エンコードのみのOCamlの文字とは対照的です。Rustの他のリテラルフォームは同記事内で後述するリテラルのセクションで示します。

プリミティブ型

Rust Haskell OCaml ディスクリプション
() () unit Unit型
bool Bool bool Boolean型
int Int int 符号付き整数型、機種依存
uint Word   符号なし整数型、機種依存
i8 Int8   符号付き8ビット整数、2の補数形式
i16 Int16   符号付き16ビット整数、2の補数形式
i32 Int32 int32 符号付き32ビット整数、2の補数形式
i64 Int64 int64 符号付き64ビット整数、2の補数形式
u8 Word8 char 符号なし8ビット整数
u16 Word16   符号なし16ビット整数
u32 Word32   符号なし32ビット整数
u64 Word64   符号なし64ビット整数
f32 Float   32ビットIEEE浮動小数点
f64 Double float 64ビットIEEE浮動小数点
char Char   Unicodeスカラ値(サロゲートなしのコード点)
str     UTF-8エンコード済み文字列

Rustではstr型は特別です。プリミティブなので、コンパイラは特定の文字列演算を最適化することができます。しかし、第一級ではないために、str型の型変数を定めたり、関数に直接str型の値を渡すことができません。プログラムでRustの文字列を使用するには、後で述べるように文字列参照を使用しなければいけません。

演算子の等価性:

==  !=  <  >  <=  >=  && ||   // Rust
=   <>  <  >  <=  >=  && ||   (* OCaml *)
==  /=  <  >  <=  >=  && ||   {- Haskell -}

+  +  +  +  -  -  *  *  /     /  %     !    // Rust
+  +. @  ^  -  -. *  *. /     /. mod   not  (* OCaml *)
+  +  ++ ++ -  -  *  *  `div` /  `mod` not  {- Haskell -}

&    |   ^    <<     >>     !          // Rust
land lor lxor [la]sl [la]sr lnot       (* OCaml *)
.&.  .|. xor  shiftL shiftR complement {- Haskell -}

Rustはブーリアン演算と、整数型上での単一ビットごとのNOT演算子の両方に!を使います。単一の~はRustでは、以下で説明するように、C言語とは違った意味を持ちます。

複合式:

Rust OCaml Haskell Name
R{a:10, b:20} {a=10; b=20} R{a=10, b=20} レコード式
R{a:30, ..z} {z with a=30} z{a=30} アップデート関数を伴うレコード
(x,y,z) (x,y,z) (x,y,z) タプル式
x.f x.f f x フィールド式
[x,y,z] [|x;y;z|]   配列式、固定サイズ
[x, ..10]     配列式、初期値で繰り返し固定化
x[10] x.(10) x!10 インデックス式(ベクター/配列)
x[10] x.[10] x!!10 インデックス式(文字列)
{x;y;z} begin x;y;z end   ブロック式
{x;y;} begin x;y;() end   ブロック式(unitで終わる)

ブロック式の値は、ブロック内の最後の式の値になることに注意してください。ブロックがセミコロンで終わる場合は、その値は()になります。

関数の型と定義:

// Rust
// f : |int,int| -> int
fn f (x:int, y:int) -> int { x + y };

// fact : |int| -> int
fn fact (n:int) -> int {
   if n == 1 { 1 }
   else { n * fact(n-1) }
}
(* OCaml *)
(* val f : int * int -> int *)
let f (x, y) = x + y

(* val fact : int -> int *)
let rec fact n =
    if n = 1 then 1
    else n * fact (n-1)

{- Haskell -}
f :: (Int, Int) -> Int
f (x, y) = x + y

fact :: Int -> Int
fact n =
    if n = 1 then 1
    else n * fact(n-1)

パターンマッチとガード

// Rust
match e {
  0           => 1,
  t @ 2       => t + 1,
  n if n < 10 => 3,
  _           => 4
}

(* OCaml *)
match e with
 | 0             -> 1
 | 2 as t        -> t + 1
 | n when n < 10 -> 3
 | _             -> 4

{- Haskell -}
case e of
  0          -> 1
  t @ 2      -> t + 1
  n | n < 10 -> 3
  _          -> 4

副作用を伴う再帰

// Rust
fn collatz(n:uint) {
  let v = match n % 2 {
     0 => n / 2,
     _ => 3 * n + 1
  }
  println!("{}", v);
  if v != 1 { collatz(v) }
}
fn main() { collatz(25) }   

(* OCaml *)
let rec collatz n =
    let v = match n % 2 with
       | 0 -> n / 2
       | _ -> 3 * n + 1
    in
    Printf.printf "%d\n" v;
    if v <> 1 then collatz v

let _ = collatz 25  

{- Haskell -}
collatz n = do
    let v = case n `mod` 2 of
            0 -> n `div` 2
            _ -> 3 * n + 1

    putStrLn $ show v
    when (v /= 1) $ collatz v

main = collatz 25

明らかに、OCamlと同様に、Rustは正格(先行)評価を行い、関数が副作用のある式を含むことが可能です。

Rustが用いるLLVMコードジェネレータは上記の関数に対して末尾呼び出しの除去ができますが、Rustは末尾呼び出しの除去を(現在のところ)保証していないことに注意してください。不確かな場合は、下記が同等です。

let mut n = 25u;
while n != 1 {
   n = if n % 2 == 0 { n / 2 }
       else { 3 * n + 1 }
   println("{}", n);
}

レコード型の式とフィールドアクセス

// Rust
struct Point {
  x : int,
  y : int
}

let v = Point {x:1, y:2};
let s = v.x + v.y   
(* OCaml *)
type Point = {
  x : int;
  y : int
}

let v = { x = 1; y = 2 };
let s = v.x + v.y   

{- Haskell -}
data Point = Point {
   x :: Int,
   y :: Int
}

v = Point {x = 1, y = 2}
s = (x v) + (y v)

自由な型パラメータ(ジェネリックなデータと関数型)

// Rust
type Pair<a,b> = (a,b)

// id<t> : |t| -> t
fn id<t>(x : t) -> t { x }  
(* OCaml *)
type ('a, 'b) pair = 'a * 'b

(* val id : 't -> 't *)
let id x = x    
{- Haskell -}
type Pair a b = (a, b)

id :: t -> t
id x = x

代数的データ型

// Rust
enum Option<T> {
   None,
   Some(T)
}
// x : Option<t>
match x {
  None    => false,
  Some(_) => true
}

 (* OCaml *)
type 't option =
    None
  | Some of 't

(* x : t option *)
match x with
  | None -> false
  | Some _ -> true

{- Haskell -}
data Maybe a =
    Nothing
  | Just a

{- x : Maybe t -}
case x of
   Nothing -> False
   Just _ -> True

ラムダ式と高階関数

// Rust
// ||int,int| -> int, int| -> int
fn ff(f:|int,int|->int, x:int) -> int
{ f (x, x) }

// m2 : |int| -> int
fn m2(n : int) -> int
{ ff ((|x,y| { x + y }), n) }
 (* OCaml *)
(* (int*int->b)*int -> int *)
let ff (f, x) =
    f (x, x)

(* m2 : int -> int *)
let m2 n =
    ff ((fun(x,y) -> x + y), n)
{- Haskell -}
ff :: ((int,int)->int, int) -> int
ff (f, x) =
   f (x, x)

m2 :: Int -> Int
m2 n =
   ff ((\(x,y) -> x + y), n)

トレイト:Rustの型クラス

Rustの“トレイト”はHaskellの型クラスに似ています。

Haskellと主に違う点は、トレイトはドット表記法の式に対してのみ介在するということです。すなわちa.foo(b)のような形です。

しかしながら、C++、Java、C#、OCamlのプログラマは、トレイトを伝統的なオブジェクトクラスと混同してはいけません。トレイトはまさに型クラスです。つまり、トレイトを、プリミティブ型を含む任意のデータ型に追加することができます。

// Rust
trait Testable {
    fn test(&self) -> bool
}

impl Testable for int {
    fn test(&self) -> int {
       if *self == 0 { false }
       else { true }
    }
}

fn hello(x:int) -> bool {
   x.test()
}   
{- Haskell -}
class Testable a where
    test :: a -> Bool


instance Testable Int where
    test n =
       if n == 0 then False
       else True


hello :: Int -> Bool
hello x =
    test x

トレイトのメソッド宣言において、識別子“self”は、そのメソッドが適用される実際のオブジェクトを示します。

Haskellと同様に、Rustのトレイトは演算子のオーバーロードに使うことができます。例えば、Peano整数に対して新たな直和型を定義する場合は、

// Rust
enum Peano {
   Zero,
   Succ(Box<Peano>)
}   
{- Haskell -}
data Peano =
    Zero
  | Succ Peano

そして、PartialEqクラスを例示することで、比較演算子==をPeano整数の間にオーバーロードできます。

// Rust
impl PartialEq for Peano {
    fn eq(&self, other:&Peano) -> bool {
      match (self, other) {
        (&Zero, &Zero)               => true,
        (&Succ(ref a), &Succ(ref b)) => (a == b),
        (_, _)                       => false
      }
    }
}
{- Haskell -}
instance Eq Peano where
    (==) self other =
        case (self, other) of
          | (Zero, Zero)     -> True
          | (Succ a, Succ b) -> (a == b)
          | (_, _)           -> False

また、Haskellと同様に、トレイトはメソッドに対するデフォルト実装を提供できます。インスタンスが特殊化を省略する時に使用されます。

// Rust
trait PartialEq {
   fn eq(&self, other:&Self) -> bool;
   fn ne(&self, other:&Self) -> bool
   { !self.eq(other) }
 }
{- Haskell -}
class Eq a where
   (==) : a -> a -> Bool
   (!=) : a -> a -> Bool
   (!=) x y = not (x == y)

トレイト宣言内のメソッド宣言において、識別子“self”はそのトレイトが適用される実際の型を指します。

Rustにおいてオーバーロード可能な各演算子には、標準ライブラリの中に対応するトレイトがあります。

拡張 トレイト 同等のHaskellクラス/メソッド
a == b a.eq(b) std::cmp::PartialEq class PartialEq a where (==) : a -> a -> bool
a != b a.ne(b) std::cmp::PartialEq class PartialEq a where (/=) : a -> a -> bool
a <  b a.lt(b) std::cmp::PartialOrd class PartialOrd a where (<) : a -> a -> bool
a >  b a.gt(b) std::cmp::PartialOrd class PartialOrd a where (>) : a -> a -> bool
a <= b a.le(b) std::cmp::PartialOrd class PartialOrd a : Eq a where (<=) : a -> a -> bool
a >= b a.ge(b) std::cmp::PartialOrd class PartialOrd a : Eq a where (>=) : a -> a -> bool
a + b a.add(b) std::ops::Add<b,c> class Add a b c where (+) : a -> b -> c
a - b a.sub(b) std::ops::Sub<b,c> class Sub a b c where (-) : a -> b -> c
a * b a.mul(b) std::ops::Mul<b,c> class Mul a b c where (*) : a -> b -> c
a / b a.div(b) std::ops::Div<b,c> class Div a b c where (/) : a -> b -> c
a % b a.rem(b) std::ops::Rem<b,c> class Rem a b c where (%) : a -> b -> c
-a a.neg() std::ops::Neg<c> class Neg a c where (-) : a -> c
!a a.not() std::ops::Not<c> class Not a c where (!) : a -> c
*a a.deref() std::ops::Deref<c> class Deref a c where (*) : a -> c
a & b a.bitand(b) std::ops::BitAnd<b,c> class BitAnd a b c where (&) : a -> b -> c
a | b a.bitor(b) std::ops::BitOr<b,c> class BitOr a b c where (|) : a -> b -> c
a ^ b a.bitxor(b) std::ops::BitXor<b,c> class BitXor a b c where (^) : a -> b -> c
a << b a.shl(b) std::ops::Shl<b,c> class Shl a b c where (<<) : a -> b -> c
a >> b a.shr(b) std::ops::Shr<b,c> class Shr a b c where (>>) : a -> b -> c

forループは次のように、特殊なトレイトstd::iter::Iteratorを使用します。


// Rust // the following expression: [<label>] for <pat> in <iterator expression> { body...; } // ... expands to (internally): match &mut <iterator expression> { _v => [label] loop { match _v.next() { None => break, Some(<pat>) => { body... } } } }

メソッドnextIteratorが実装します。nextの戻り値の型はOptionです。これは、“繰り返すものが残っていない”ことを意味する値None、あるいは次の繰り返し値がxであることを意味する値Some(x)を持つことができます。

アドホックなオブジェクトとメソッド

トレイトが提供する仕組みに加えて、どのstructenumも、“impl”を使って1つ以上のメソッドインターフェースを使ってデコレートすることができます。デコレートは定義から分離され,異なるモジュール内ででデコレートすることができます。

// Rust
struct R {x:int};

// in some module:
impl R {
 fn hello(&self) {
  println!("hello {}", self.x);



 }
}
// possibly somewhere else:
impl R {
 fn world(&self) {
  println!("world");
 }
}
// Example use:
fn main() {
  let v = R {x:10};
  v.hello();
  v.world();

  (R{x:20}).hello();
}   

// Rust
enum E {A, B};

// in some module:
impl E {
 fn hello(&self) {
   match self {
    &A => println!("hello A"),
    &B => println!("hello B")
   }
 }
}
// possibly somewhere else:
impl E {
 fn world(&self) {
  println!("world");
 }
}
// Example use:
fn main() {
  let v = A;
  v.hello();
  v.world();

  B.hello();
}

安全な参照

C言語と同様に、Rustの関数パラメータはデフォルトで、値によって渡されます。大きなデータ型では、データのコピーはコストが高くなるかもしれないため、代わりに参照を使うことも考えられます。

Tのどんなオブジェクトvでも、式“&v”を使ってそのオブジェクトを指す参照を作ることができます。その参照自体はその時、型&Tを持つことになります。

use std::num::sqrt;
struct Pt { x:f32, y:f32 }

// This works, but may be expensive:
fn dist1(p1 : Pt, p2: Pt) -> f32 {
   let xd = p1.x - p2.x;
   let yd = p1.y - p2.y;
   sqrt(xd * xd + yd * yd)
}

// Same, using references:
fn dist2(p1 : &Pt, p2: &Pt) -> f32 {
   let xd = p1.x - p2.x;
   let yd = p1.y - p2.y;
   sqrt(xd * xd + yd * yd)
}

// Usage:
fn main() {
   let a = Pt { x:1.0, y:2.0 };
   let b = Pt { x:0.0, y:3.0 };
   println!("{}", dist1(a, b));
   println!("{}", dist2(&a, &b));
}

この例で示しているように、Rustはstruct参照のためのシンタックスシュガーを提供しています。つまりp1の型が&PtでありPtxという名前のフィールドを持つ場合は、p1.xと書くことも可能です。このシンタックスシュガーはメソッド呼び出し(x.foo())にも利用できます。

しかし、他の多くのケースでは、参照される値が*単行演算子で“取得される”か、パターンが&でマッチしなければなりません。

fn add3(x : &int) -> int {
   println!("x : {}", *x); // OK
   3 + x; // invalid! (+ cannot apply to &int)
   3 + *x; // OK
}

fn test<t>(x : &Option<t>) -> bool {
   match *x {
     None     => false,
     Some(_)  => true
   }
   // Also valid, equivalent:
   match x {
     &None    => false,
     &Some(_) => true
   }
}

単純な参照では下部のオブジェクトの変更を許していません。このため、参照によって変更できることが望ましい場合は、次のように“&mut”を使用します。

fn incx(x : &int) {
   x = x + 1;   // invalid! (mismatched types in "int& = int")
   *x = *x + 1; // invalid! (*x is immutable)
}

fn inc(x : &mut int) {
   x = x + 1;   // invalid! (x is immutable)
   *x = *x + 1; // OK
}

fn main() {
   inc(&3); // invalid! (3 is immutable)
   inc(&mut 3); // OK, temp var forgotten after call
   let v = 3;
   inc(&v); // invalid! (v is immutable)
   inc(&mut v); // OK, temp var forgotten after call
   let mut w = 3;
   inc(&w); // OK
}

Rustの型システムは、参照を通した可変な別名を許しません。つまり、C言語とは違って、参照で別名を使って同じオブジェクトを変更することはできません。これは借用という概念を通して行われます。つまり、オブジェクトの所有権が参照によって借用されている間は、元の変数を使用できないということです。例を挙げます。

fn main() {
    let mut a = 1;
    a = 2; // OK
    println!("{}", a); // OK, prints 2
    // {...} introduces a new scope:
    {
      let ra = &mut a;
      a = 3;   // invalid! (a is borrowed by ra)
      *ra = 3; // OK
      println!("{}", a); // invalid! (a is borrowed)
      println!("{}", *ra); // OK, prints 3
    };
    // end of scope, ra is dropped.
    println!("{}", a); // OK, a not borrowed any more (prints 3)
}

“すべての値はデフォルトでは不変”であることや所有権・借用のルールと共に、参照型はRustの型システムの核となる特徴です。この特徴によって、Rustの型システムはC言語よりも根本的に安全なものとなっています。

寿命と記憶域、そして管理オブジェクト


本セクションで示す概念とシンタックスは、Rustのバージョン0.11で追加されたものです。Rust 0.10以前のバージョンは異なる概念、用語、シンタックスを使用しています。
また、この記事の執筆時点(2014年7月)において、公式のRustマニュアルとチュートリアルは最新の言語実装のものにまだ更新されていません。本セクションはマニュアルではなく実装に即しています。

はじめに

RustはC言語と同じ抽象機械モデルについて定義されています。この抽象機械にはメモリのセグメントがあり、言語のランタイム機構はプログラムの実行中にメモリのセグメントの割り当てと解除を行うことができます。

抽象機械において、RustではC言語と同様に下記の2つの概念が定義されています。

  • オブジェクトの記憶域によって、そのオブジェクトが格納されるメモリの型が決まります。
  • オブジェクトの寿命つまり記憶期間は、オブジェクトが割り当てられた点から解除された点までの時間のセグメントです。

C言語におけるメモリ関連の問題はすべて、Cプログラムでは寿命外(すなわち割り当て前または解除後)あるいは記憶域外(すなわちメモリの下位アドレスまたは上位アドレス)のオブジェクトを指す参照を操作できることから起こっています。

Rustは、オブジェクトが寿命外または記憶域外で使用できないことを確実にすることで、そうした問題を徹底的に防ぐようにしています。

Rustの記憶域とC言語の記憶域

C言語やRustの抽象機械には、4種類の記憶域、つまりstatic(静的)、thread(スレッド)、automatic(自動)、allocated(割り当て)があります。

記憶域 使用メモリの型 C言語での例
static プロセス全体の特殊セグメント static int i;
automatic スタック int i; (in function scope)
allocated グローバルヒープ int *i = malloc(sizeof(int));
thread スレッドごとのヒープ _Thread_local int i;

C言語では、オブジェクトの寿命はその記憶域だけで決まります。

記憶域 C言語での寿命
static プログラムの開始から終了まで
automatic スコープの始まりから終わりまで、1駆動フレームにつき1
allocated mallocからfreeまで(またはmmapからmunmapまで、など)
thread オブジェクト割り当てからスレッド終了まで

Rustでは、staticとautomaticのオブジェクトの寿命はC言語と同じです。しかしながら、

  • Rustは新たに“Box”型を導入しています。ヒープに割り当てられたオブジェクトのための専用の構文をもち、そのオブジェクトは管理オブジェクトと呼ばれます。

Rustは、さまざまな型付けルールに関連して、ボックスに対する多くの管理戦略をサポートしています。

  • デフォルトのボックス管理戦略は、ボックスの所有者が1人だけであることを保証します。そうすればコンパイラは、いつボックスの寿命が終わるかを正確に把握でき、参照カウントやガベージコレクションなどのようにシステムに余計な負荷をかけることなく、割り当てを安全に解除することができます。

  • もう1つの戦略は、遅延ガベージコレクションを使うGCです。Rustのランタイムシステムが不要だと決めた時点で、GCボックス用の記憶域が再生され再利用が可能になります(再生には想定外の時間がかかるかもしれません)。GCボックスへの参照は1つだけ(ユニーク)である必要はありません。GCボックスは、グラフのような共通のノードを持った複雑なデータ構造を構築する上で適切な型です。

  • C言語とは違い、Rustはスレッド間での管理オブジェクトの共有を許しません。Erlangと同様に、オブジェクトは割り当て毎に1人の所有者を持ちます。そしてほとんどのオブジェクトは、それぞれのスレッドだけで使われます。これによって、大抵の場合はデータの競合を防ぐことができます。また、1つのスレッドに所有権を限定することにより、GCオブジェクトのガベージコレクションをスレッド間で同期する必要がありません。つまり実装が簡単で、動作を速くできます。

  • 通常のオブジェクトへの参照と同様に、管理オブジェクトを参照している可変な別名をRustは許しません。

  • ベクターへのアクセスはすべてチェックされ、参照はポインタの演算をサポートしません。

  • Rustは管理されていないポインタを使用しないよう強く推奨しています。安全ではない特性があるとして、システムでコンパイラに警告を誘発させます。

ボックスの作成と使用

Rustの管理オブジェクトの扱い方は、比較的シンプルです。“puts objects into boxes”の式にあるboxキーワードによって、オブジェクトの寿命が動的に管理されます。ボックスの不可変性と所有権については、前述した参照と同様の考え方です。

fn main() {

  let b = box 3i; // b has type Box<int>
  b = 4i; // invalid! can't assign int to box<int>
  *b = 4i; // invalid! b is immutable, so is the box
  let mut c = box 3i; // c has type mut Box<int>
  *c = 4i; // OK

  let v = box vec!(1i,2,3); // r1 has type Box<Vec<int>>
  *v.get_mut(0) = 42; // invalid! v is immutable, so is the box
  let mut w = box vec!(1i,2,3);
  *w.get_mut(0) = 42; // OK, rust 0.11.1+ also permits w[0] = 42

  let z = w; // z has type Box<Vec<int>>, captures box
  println!("{}", w); // invalid! box has moved to z

  w = box vec!(2i,4,5); // overwrites reference, not box contents
  println!("{} {}", w, z); // OK, prints [2,4,5] [42,2,3]
}

boxキーワードは、実際にはbox(HEAP)の省略形です。“box(A) E”は、Aによって割り当てられたメモリオブジェクトにEの値の結果を置くことを意味し、これがトレイトです。Rustのバージョン0.11において、これ以外に標準ライブラリ内にあるアロケータは、ガベージコレクションされたオブジェクトであるGCだけです。

意味
box v Box<T> “v”のコピーへのユニークな参照。”box(HEAP) v”の省略形
box(GC) v Gc<T> “v”のコピーへのガベージコレクションされたスマートポインタ
box(A) v ??<T> スマートポインタの一種。”A”は特殊なトレイトを実装する

再帰データ構造

管理オブジェクトは、適切な再帰的代数的データ型を実装するための”失われた環”になっています.

// Rust
enum Lst<t> {
   Nil,
   Cons(t, Box<Lst<t>>)
}
fn len<t>(l : &Lst<t>) -> uint {
   match *l {
     Nil => 0,
     Cons(_, ref x) => 1 + len(*x)
   }
}

fn main() {
   let l = box Cons('a',
            box Cons('b',
             box Nil));
   println!("{}", len(l));
}   
 (* OCaml *)
type 't lst =
    Nil
  | Cons of 't * 't lst

let len l =
  match l with
  | Nil -> 0
  | Cons(_, x) -> 1 + len x




let l = Cons('a',
          Cons('b',
              Nil));
print_int (len l)   
{- Haskell -}
data Lst t =
    Nil
  | Cons t (Lst t)

let len l =
  case l of
    Nil -> 0
    Cons _ x -> 1 + len x



main = do
         let l = Cons 'a'
                      (Cons 'b'
                            Nil)
         putStrLn $ show l

オブジェクトの共有:RcとArc

ボックスが提供する利便性を補完するものとして、Rustの標準ライブラリには、参照がカウントされる不変オブジェクトへのラッパーが2つ実装されています。これにより複数のオーナーからの参照が可能になります。

use std::rc::Rc;
fn main() {
    let b = Rc::new(3i);
    let c = &b;
    println("{} {}", b, c); // OK
}

参照カウントを使えば、最後の参照が終了したその時に、オブジェクトの割り当てが解除されたことが保証されます。ボックスの使用のトレードオフは、新しい参照が作られたり参照が終わったりするたびに、参照カウントを更新しなければならない、ということです。

std::rc::Rcstd::arc::Arcという2つの実装が提供されています。どちらも同じインターフェースを提供します。なぜ同じようなものがあるかというと、プログラマにパフォーマンス上のトレードオフを与えるためです。Rcはメモリを使いません。だから軽量で、つまり速いのです。しかしスレッド間で共有できません。Arcはメモリを使います。Rcと比べるとやや効率が悪いですが、スレッド間でデータを共有できます。

マクロとメタプログラミング

マクロを定義する基本的なシンタックスは次のようになります。

macro_rules! MACRONAME (
    (PATTERN...) => (EXPANSION...);
    (PATTERN...) => (EXPANSION...);
    ...
)

例えば、次に示すマクロは、forループをPascalのように定義したものです。

macro_rules! pfor (
   ($x:ident = $s:expr to $e:expr $body:expr)
       => (match $e { e => {
              let mut $x = $s;
              loop {
                   $body;
                   $x += 1;
                   if $x > e { break; }
              }
          }});
);

// Example use:
fn main() {
     pfor!(i = 0 to 10 {
         println!("{}", i);
     });
}

このマクロが、ローカルネームを付けるに当たってどのようにマッチステートメントを使っているかに注目してください。これによって1度しか値を返さないようになっています。

Scheme同様、マクロも再帰可能です。例えば次に示すマクロでは、stepがあってもなくてもpforを実装するために再帰を使っています。

macro_rules! pfor (
   ($x:ident = $s:expr to $e:expr step $st:expr $body:expr)
       => (match $e,$st { e, st => {
              let mut $x = $s;
              loop {
                   $body;
                   $x += st;
                   if $x > e { break; }
              }
          }});
   ($x:ident = $s:expr to $e:expr $body:expr)
      => (pfor!($x = $s to $e step 1 $body));
);

// Example use:
fn main() {
     pfor!(i = 0 to 10 step 2 {
         println!("{}", i);
     });
}

マクロは可変個引数であり、シンタックス内の任意の繰り返しは、マクロの1つの引数で記録できます。例えば、次に示すマクロでは、各引数においてprintln!を呼び出し、それは任意の型になり得ます。

macro_rules! printall (
     ( $( $arg:expr ),*  ) => (
          $( println!("{}", $arg) );*
     );
);

// example use:
fn main() {
    printall!("hello", 42, 3.14);
}

シンタックスは次のようになります。マクロ式の左側(pattern)では、$( PAT )DELIM*が、DELIMによって区切られているPATのゼロまたはそれ以上の出現に一致し、右側(expansion)では、$( TEXT )DELIM*が、DELIMによって区切られたTEXTの1回またはそれ以上の繰り返しを展開します。展開される繰り返しの回数は、含まれるマクロの引数がマッチする回数によって決まります。示した例の中では、それぞれの引数(カンマによって区切られる)は、セミコロンによって区切られるprintln!という呼び出しで代用されています。

リテラル

Rustにはさまざまな数値リテラルの語彙があります。

Rustのシンタックス 同義 Haskellでのシンタックス OCamlでのシンタックス
123i   int 123 :: Int 123
123u   uint 123 :: Word  
123i8   i8 123 :: Int8  
123u8   u8 123 :: Word8 Char.chr 123
123i16   i16 123 :: Int16  
123u16   u16 123 :: Word16  
123i32   i32 123 :: Int32 123l
123u32   u32 123 :: Word32  
123i64   i64 123 :: Int64 123L
123u64   u64 123 :: Word64  
1_2_3_4 1234 (integer)    
1234_i 1234i int    
0x1234 4660 (integer) 0x1234 0x1234
0x1234u16 4660u16 u16 0x1234 :: Word16  
0b1010 10 (integer)   0b1010
0o1234 668 (integer) 0o1234 0o1234
b'a' 97u8 u8   'a'
b"a" [97u8] [u8]    
12.34   (float) 12.34  
12.34f32   f32 12.34 :: Float  
12.34f64   f64 12.34 :: Double 12.34
12e34 1.2e35 (float) 12e34  
12E34 1.2e35 (float) 12E34  
12E+34 1.2e35 (float) 12E+34  
12E-34 1.2e-33 (float) 12E-34  
1_2e34 12e34 (float)    
1_2e3_4 12e34 (float)    

文字、バイト、文字列によるエスケープのリテラル

シンタックス 同義 シンタックス 同義 シンタックス 同義
'\x61' 'a' b'\x61' 97u8 "\x61" "a"
'\\' '\x5c' b'\\' 92u8 "\\" "\x5c"
'\'' '\x27' b'\'' 39u8 "\"" "\x22"
'\0' '\x00' b'\0' 0u8 "\0" "\x00"
'\t' '\x09' b'\t' 9u8 "\t" "\x09"
'\n' '\x0a' b'\n' 10u8 "\n" "\x0a"
'\r' '\x0d' b'\r' 13u8 "\r" "\x0d"
'\u0123'       "\u0123"  
'\U00012345'       "\U00012345"  

C言語ファミリでよく使われるエスケープ(\a、\fなど)や、8進法表記のエスケープ(例、\0123)は、Rustでは使えないことに注意してください。

最後に、RustはPython同様に、raw文字列、複数の文字列のデリミタをサポートしており、これにより文字列内のデリミタのオカレンスを引用することを避けています。

シンタックス 文字列の値 シンタックス
"foo" foo b"foo" [102u8,111,111]
"fo\"o" fo"o b"fo\"o" [102u8,111,34,111]
r"fo\n" fo\n rb"fo\n" [102u8,111,92,110]
r#"fo\"o"# fo\"o rb#"fo\"o"# [102u8,111,92,34,111]
"foo#\"#bar" foo#"#bar b"foo#\"#bar" [102u8,111,111,35,34,35,98,97,114]
r##"foo#"#bar"## foo#"#bar rb##"foo#"#bar"## [102u8,111,111,35,34,35,98,97,114]

謝辞

RedditやHacker Newsでクオリティの高いコメントを寄せてくれた多くの人たちに感謝します。この記事の初版に磨きをかけるに当たって、いただいたコメントが大変参考になりました。

参照

Copyright © 2014, Raphael ‘kena’ Poss. Permission is granted to distribute, reuse and modify this document according to the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/.