2014年8月27日
関数型プログラマのためのRust
本記事は、原著者の許諾のもとに翻訳・掲載しております。
この投稿はEdward Z. Yangが2010年に書いた OCaml for Haskellers 、私自身が今年頭に書いた Haskell for OCaml programmers の流れに沿っています。
目次
- プロローグ
- なぜRustを学ぶべきか
- 直接対応が可能なもの
- トレイト:Rustの型クラス
- アドホックなオブジェクトとメソッド
- 安全な参照
- 寿命と記憶域、そして管理オブジェクト
- オブジェクトの共有:RcとArc
- マクロとメタプログラミング
- リテラル
- 謝辞
- 参照
- Copyright and licensing
注 : この記事の最新版は下記サイトで見られます。
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> |
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... }
}
}
}
メソッド next
は Iterator
が実装します。 next
の戻り値の型は Option
です。これは、“繰り返すものが残っていない”ことを意味する値 None
、あるいは次の繰り返し値が x
であることを意味する値 Some(x)
を持つことができます。
アドホックなオブジェクトとメソッド
トレイトが提供する仕組みに加えて、どの struct
や enum
も、“ 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
であり Pt
が x
という名前のフィールドを持つ場合は、 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::Rc
と std::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でクオリティの高いコメントを寄せてくれた多くの人たちに感謝します。この記事の初版に磨きをかけるに当たって、いただいたコメントが大変参考になりました。
参照
- The Rust Language Tutorial , version 0.10, April 2014.
- The Rust Language Tutorial , version 0.11.0, July 2014.
- The Rust Guide , version 0.11.0, July 2014.
- Aaron Turon, Rust Guidelines , 2014.
- Will Yager. Why Go is not good , 2014. Explains how Go lacks many features found in Rust and thereby fails to be a modern functional language.
- Edward Z. Yang, OCaml for Haskellers , October 2010.
- Raphael ‘kena’ Poss, Haskell for OCaml programmers , March 2014.
- Xavier Leroy et al., The OCaml system release 4.01 , September 2013.
Copyright and licensing
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/ .
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa