1 of 42

自作OSで学んだRustのあれこれ

by だいみょーじん

2 of 42

自己紹介

  • だいみょーじん
  • 自動車業界のエンジニア
  • THEOS(テオス)というOSを作っている
  • 今年からHeliOSというOSも作り始めた
  • 好きな言語:Rust,Haskell
  • OS自作意外の趣味:読書(歴史,神話,宗教)

3 of 42

目次(自作OSで学んだRustのあれこれを5つ紹介!)

  • cargo clippyコマンド
  • cargo docコマンド
  • bitfield_structクレート
  • OnceCell
  • RustをHaskellの観点から見てみる

4 of 42

cargo clippy

  • Rustのlint(静的解析ツール)
  • もっとイケてる書き方を提案してくれる

そのブール式,もっと簡単にできるよ!

x.map(..).flatten()はx.flat_map(..)でいいよ!

5 of 42

cargo clippy

  • Rustのlint(静的解析ツール)
  • もっとイケてる書き方を提案してくれる

推奨された書き方の利点

  • alignが一回しか現れない
  • SVO形式で英語のように読める

6 of 42

cargo doc

  • ドキュメントの自動生成
  • Cなどのdoxygenやpythonのsphinx�みたいなやつ

7 of 42

bitfield_struct クレート

  • ビット単位の構造体
  • #[bits(n)]でnビットのフィールドを定義
  • access = ROで読み込み専用
  • access = WOで書き込み専用
  • self.<フィールド名>()で読み込み
  • self.set_<フィールド名>(値)で書き込み
  • self.with_<フィールド名>(値)で�そのフィールドだけ書き換えた�新しいインスタンスを生成
  • Self::<大文字フィールド名>_OFFSET
  • Self::<大文字フィールド名>_BITS
  • From/Into<u**>/Debugトレイト自動実装

8 of 42

OnceCell

  • 一度だけ初期化できる値
  • 初期化処理をクロージャとして渡すことにより実現

9 of 42

RustをHaskellの観点から見てみる

  • RustとHaskellを比較するために,まずはHaskell入門(しばらくHaskellの話が続きます...)
    • Haskellの関数は2種類ある

  • 以降,純粋関数を関数,不純関数をIOと言います.

返り値の依存先(返り値が何によって決まるのか)

純粋関数(関数)

引数のみ

不純関数(IO)

引数,スタティックなローカル変数,グローバル変数,ユーザの入力,etc.

10 of 42

RustをHaskellの観点から見てみる

  • Haskellの関数適用(関数呼び出し)
    • Haskellの関数は全てカリー化されていて,ひとつの引数しかとらない.
    • Haskellの関数適用は,引数を括弧で囲まない.
    • Haskellの関数適用は,左結合
  • Haskellの演算子は中置記法化された関数で,括弧に入れると前置記法に戻すことができる.
    • 例えば,「1 + 2 == 3」は,「(==) ((+) 1 2) 3」とも書ける.
  • Haskellでは左右どちらかの項が欠けた二項演算を括弧で囲んだものも関数とみなす.
    • 例えば(2+)と(+2)はどちらも「値を受け取って2を足して返す関数」を意味する.
  • Haskellの関数はバッククォートで囲むと中置記法化できる.
    • f x y == x `f` y

C言語の表記

C言語の表記(カリー化)

Haskellの表記(括弧付き)

Haskellの表記(括弧無し)

f(x)

f(x)

f x

f x

f(x, y)

f(x)(y)

(f x) y

f x y

f(x, y, z)

f(x)(y)(z)

((f x) y) z

f x y z

11 of 42

RustをHaskellの観点から見てみる

  • 型を対象とし,関数を射とする圏(型と関数から構成される世界みたいなもの)

12 of 42

RustをHaskellの観点から見てみる

  • モナド:文脈に包まれた型

  • Maybe Int型の値
    • Just 1
    • Nothing
  • [String]型の値
    • []
    • [“Hello”, “World”]
  • Int -> Bool型の値
    • isPrime
    • isEven
  • IO String型の値
    • getLine (「x <- getLine」のように使う)

文脈

発生源となる計算

Maybe

存在するかどうか分からない

失敗するかもしれない計算

リスト

いくつあるか分からない

解の個数が分からない計算

関数

引数を与えると返ってくる

解を得るための材料が足りない計算

IO

実行してみないと分からない

ユーザの入力や現在時刻に依存する計算

13 of 42

RustをHaskellの観点から見てみる

  • fmap関数で裸の型の圏の関数をMaybeに包まれた型の圏の関数に変換する

14 of 42

RustをHaskellの観点から見てみる

  • fmap関数で裸の型の圏の関数をリストに包まれた型の圏の関数に変換する

  • fmapは,裸の型の圏の関数を,ある文脈に包まれた型の圏の関数に変換する関数�(裸の型の圏から文脈に包まれた型の圏への関手)
  • fmapと等価な演算子<$>がある
    • fmap f x == f <$> x 「関数fをxの文脈において適用する」
    • fmap isPrime (Just 3) == (isPrime <$> Just 3)
    • fmap isPrime [1,2,3,4] == (isPrime <$> [1,2,3,4])

15 of 42

RustをHaskellの観点から見てみる

  • 失敗するかもしれない計算

  • 1 / 2 / 3 / 4を計算したい

  • Maybeに包まれているなら,<$>で関数適用すればいいのでは?

divide :: Float -> Float -> Maybe Float

divide 0 b = Nothing

divide a b = Just (b / a)

divide 4 (divide 3 (divide 2 1))  ←divideは引数としてMaybe Floatを受け取れないので✕

divide 4 <$> (divide 3 <$> divide 2 1) ←括弧の部分の評価値がMaybe Maybe Floatなので✕

16 of 42

RustをHaskellの観点から見てみる

  • 割り算の結果のMaybe文脈を引きはがしてから次の割り算に渡してほしい
    • >>=(バインド)という演算子がそれをやってくれる

  • <$>と>>=の違いを見てみる
    • 違い①:<$>と>>=は方向が逆になっている

Nothing >>= divide 3 == Nothing

Just 1 >>= divide 0 == Nothing

Just 1 >>= divide 2 == Just (1/2)

divide 2 <$> Just 1

Just 1 >>= divide 2

↑計算

↑入力

↑計算

↑入力

17 of 42

RustをHaskellの観点から見てみる

  • 違い②:<$>とflip (>>=)を比較してみる
    • flip関数は,関数の第一引数と第二引数を入れ替える関数

    • flip関数を使うと,<$>と>>=の方向を揃えることができる

divide 2 <$> Just 1

divide 2 `flip (>>=)` Just 1

↑計算

↑入力

↑計算

↑入力

flip f x y == f y x

x `flip (-)` y == y - x

18 of 42

RustをHaskellの観点から見てみる

  • 違い②:<$>とflip (>>=)を比較してみる
    • <$>とflip (>>=)の型を見てみよう

      • 文脈f,型a,型bがあるとき,<$>は,型aから型bへの関数を,型f aから型f bへの関数に変換する

      • 文脈m,型a,型bがあるとき,flip (>>=)は,型aから型m bへの関数を,型m aから型m bへの関数に�変換する

(<$>) :: (Functor f) => (a -> b) -> (f a -> f b)

flip (>>=) :: (Monad m) => (a -> m b) -> (m a -> m b)

19 of 42

RustをHaskellの観点から見てみる

  • >>=がバインドと呼ばれる理由:文脈の生じる計算を結び付けられるようにしているから

20 of 42

RustをHaskellの観点から見てみる

  • Maybe文脈における>>=の挙動は,
    • 入力がNothingなら結果もNothing
    • 入力がJust zならzを計算fに入力する

  • これを踏まえて,もう一度>>=の具体例を確認してみよう.
    • 入力が存在し,かつ計算も成功するときのみJustを返す

Nothing >>= divide 3 == Nothing

Just 1 >>= divide 0 == Nothing

Just 1 >>= divide 2 == Just (1/2)

21 of 42

RustをHaskellの観点から見てみる

  • >>=により,つなげられなかったdivideを数珠つなぎにできる

divide 2 1 >>= divide 3 >>= divide 4 == Just (1/24)

22 of 42

RustをHaskellの観点から見てみる

  • >>=の連鎖からdo記法への書き換え(ステップ①)
    • return関数:値の情報を失わない最も単純な文脈に包む
      • Maybe文脈では,return x == Just x
      • (>>= return)は,引数をそのまま返す恒等関数

    • こんな風に書き換えられる

divide 2 1 >>= divide 3 >>= divide 4

== divide 2 1 >>= divide 3 >>= divide 4 >>= return

23 of 42

RustをHaskellの観点から見てみる

  • >>=の連鎖からdo記法への書き換え(ステップ②)
    • Haskellのラムダ式:「\x -> y」で,xを受け取り,yを返す関数を意味する.
    • 任意の関数fについて,「f == \x -> f x」が成り立つ.
    • これを使って,「divide 3」「divide 4」「return」をラムダ式に書き換える.

  • >>=の連鎖からdo記法への書き換え(ステップ③)
    • ラムダ式の引数と返り値の間に改行を入れてみる

    • なんかreturnが最後に来ていて,ちょっとcの関数っぽく見えてきた?

divide 2 1 >>= divide 3 >>= divide 4 >>= return

== divide 2 1 >>= (\a -> divide 3 a) >>= (\b -> divide 4 b) >>= (\c -> return c)

divide 2 1 >>= (\a ->

divide 3 a) >>= (\b ->

divide 4 b) >>= (\c ->

return c)

24 of 42

RustをHaskellの観点から見てみる

  • >>=の連鎖からdo記法への書き換え(ステップ④)
    • 糖衣構文do記法の導入

    • do記法における<-は,「文脈の中から値を取り出す」と解釈できる
      • divideの返り値は文脈に包まれたMaybe Float
      • そこから取り出されるa, b, cは裸のFloat
      • 途中でdivideがNothingを返したら>>=がそれ以降の計算を全部スルーしてNothingを流すので,�それ以降の<-はそもそも実行されない

divide 2 1 >>= (\a ->

divide 3 a) >>= (\b ->

divide 4 b) >>= (\c ->

return c)

do

a <- divide 2 1

b <- divide 3 a

c <- divide 4 b

return c

糖衣化

25 of 42

RustをHaskellの観点から見てみる

  • >>=の連鎖と等価なdo記法

  • do記法のメリット
    • 文脈付きの計算を,あたかも文脈が付いていないかのように記述できる

divide 2 1 >>= divide 3 >>= divide 4

do

a <- divide 2 1

b <- divide 3 a

c <- divide 4 b

return c

等価

do記法はこんな感じに見える

実際には文脈に包まれた�世界で計算が行われている

さらに>>=の中身を見てみると

26 of 42

RustをHaskellの観点から見てみる

  • 全体のコードを見てみる

main :: IO ()

main = putStrLn $ show answer

answer :: Maybe Float

answer = do

a <- divide 2 1 - - 1を2で割ったものをaとする

b <- divide 3 a - - aを3で割ったものをbとする

c <- divide 4 b - - bを4で割ったものをcとする

return c - - cを返す

divide :: Float -> Float -> Maybe Float

divide 0 b = Nothing

divide a b = Just $ b / a

実行結果

Just 4.1666668e-2

↑「$」は開き括弧と同じ.

 対応する閉じ括弧は行末にあることになる.

 行末の「))))」を防ぐことができる.

27 of 42

RustをHaskellの観点から見てみる

  • もうひとつの文脈の例としてリストを考えてみる�例題:’A’,’B’,’C’という3つの文字をひとつずつ並べた全ての文字列を列挙する�答え:["CBA","BCA","CAB","ACB","BAC","ABC"]

  • まず,部品となる関数extendを作る

    • extendは,文字列を文字列のリストにする関数で,
    • AからCまでの文字のうち,
    • 元の文字列に含まれていない文字を,
    • 元の文字列の先頭に追加した文字列を列挙する.

  • 一言で言うと,「いまだ選ばれていない文字を先頭に追加した文字列を列挙する」関数

extend :: String -> [String]

extend string = (: string) <$> filter (`notElem` string) ['A'..'C']

extend "" == ["A", "B", "C"]

extend "A" == ["BA", "CA"]

extend "AB" == ["CAB"]

28 of 42

RustをHaskellの観点から見てみる

  • こんな感じで例題の答えが列挙されていく
    • extendを呼び出すたびに計算が分岐していき,答えを列挙していく

29 of 42

RustをHaskellの観点から見てみる

  • 今度は完成形を分解していく方向で見ていこう

main :: IO ()

main = putStrLn $ show answer

answer :: [String]

answer = do - - do記法では計算は分岐せず,一直線に進むと見なしてよい

a <- extend "" - - 空文字列の先頭に1文字追加した文字列をaとする

b <- extend a - - aの先頭に1文字追加した文字列をbとする

c <- extend b - - bの先頭に1文字追加した文字列をcとする

return c - - cを返す

extend :: String -> [String] - - 1つの文字列が複数の文字列に分岐する

extend string = (: string) <$> filter (`notElem` string) ['A'..'C']

実行結果

["CBA","BCA","CAB","ACB","BAC","ABC"]

30 of 42

RustをHaskellの観点から見てみる

  • do記法を>>=の連鎖に戻してみる

do

a <- extend ""

b <- extend a

c <- extend b

return c

extend "" >>= (\a ->

extend a) >>= (\b ->

extend b) >>= (\c ->

return c)

脱糖

改行削除

extend "" >>= (\a -> extend a) >>= (\b -> extend b) >>= (\c -> return c)

ラムダ削除

extend "" >>= extend >>= extend >>= return

>>=return削除

extend "" >>= extend >>= extend

31 of 42

RustをHaskellの観点から見てみる

  • extend関数を>>=で繋ぎ合わせている

extend "" >>= extend >>= extend

32 of 42

RustをHaskellの観点から見てみる

  • リスト文脈における>>=の挙動

    • 具体例

([] >>= f) == []

(x:xs >>= f) == (f x ++ (xs >>= f))

↑リスト同士の結合

↑先頭要素xとし,それ以降をxsとするリストのパターン

["A", "B", "C"] >>= extend

"A" : ["B", "C"] >>= extend

extend "A" ++ (["B", "C"] >>= extend)

["BA", "CA"] ++ ("B" : ["C"] >>= extend)

["BA", "CA"] ++ extend "B" ++ (["C"] >>= extend)

["BA", "CA"] ++ ["AB", "CB"] ++ ("C" : [] >>= extend)

["BA", "CA"] ++ ["AB", "CB"] ++ extend "C" ++ ([] >>= extend)

["BA", "CA"] ++ ["AB", "CB"] ++ ["AC", "BC"] ++ []

["BA", "CA", "AB", "CB","AC", "BC"]

33 of 42

RustをHaskellの観点から見てみる

  • リスト文脈における>>=のもうひとつの解釈
    • リストのリストをリストに変換するconcatと(f <$>)の合成関数

    • この観点で同じ具体例を計算してみる

(>>= f) == concat . (f <$>)

["A", "B", "C"] >>= extend

(>>= extend) ["A", "B", "C"]

(concat . (extend <$>)) ["A", "B", "C"]

concat (extend <$> ["A", "B", "C"])

concat [extend "A", extend "B", extend "C"]

concat [["BA", "CA"], ["AB", "CB"], ["AC", "BC"]]

["BA", "CA", "AB", "CB", "AC", "BC"]

34 of 42

RustをHaskellの観点から見てみる

  • リスト文脈における(>>= return)
    • return関数:値の情報を失わない最も単純な文脈
      • リスト文脈では,return x = [x]

    • リストの文脈においても,(>>= return)は引数をそのまま返す恒等関数

35 of 42

RustをHaskellの観点から見てみる

  • リスト文脈におけるdo記法まとめ

do記法はこんな風に見える

実際は文脈の世界で計算している

さらに>>=の中を見てみると

do

a <- extend ""

b <- extend a

c <- extend b

return c

36 of 42

RustをHaskellの観点から見てみる

  • HaskellのMaybe文脈やリスト文脈に対応するRustの概念は?

Haskell

Rust

Maybe

Option

Nothing

None

Just x

Some(x)

リスト

Iterator

↑VecではなくIterator

遅延評価で,評価時に有限になれば評価前に長さ無限であっても良いなど�共通点がVecよりも多い

37 of 42

RustをHaskellの観点から見てみる

  • Haskellの<$>はRustには存在する?
    • Haskellのようにあらゆる文脈で扱える統一的な演算はなさそうだが,個別には用意されている

Haskell

Rust

Maybe文脈における<$>

Optionのmapメソッド

リスト文脈における<$>

Iteratorのmapメソッド

「f <$> x」または「fmap f x」

「x.map(f)」

「関数fをxの文脈において適用する」�「関数fをxの文脈の世界に投影する」

「xの中の値に関数fを適用する」

38 of 42

RustをHaskellの観点から見てみる

  • Haskellの>>=はRustには存在する?
    • Haskellのようにあらゆる文脈で扱える統一的な演算はなさそうだが,個別には用意されている

    • 実際にコードを比較してみよう!

Haskell

Rust

Maybe文脈における>>=

Optionのand_thenメソッド

リスト文脈における>>=

Iteratorのflat_mapメソッド

39 of 42

RustをHaskellの観点から見てみる

  • 1/2/3/4のコードをHaskellとRustで比較してみる

main :: IO ()

main = putStrLn $ show answer

answer :: Maybe Float

answer = do

a <- divide 2 1

b <- divide 3 a

c <- divide 4 b

return c

divide :: Float -> Float -> Maybe Float

divide 0 b = Nothing

divide a b = Just $ b / a

fn main(){

println!("{:?}", answer());

}

fn answer() -> Option<f64> {

divide(2.0, 1.0)

.and_then(|a| divide(3.0, a))

.and_then(|b| divide(4.0, b))

.and_then(|c| Some(c))

}

fn divide(a: f64, b : f64) -> Option<f64> {

match a {

0.0 => None,

a => Some(b / a),

}

}

Haskell

Rust

40 of 42

RustをHaskellの観点から見てみる

  • ABC並べ替えのコードをHaskellとRustで比較してみる

main :: IO ()

main = putStrLn $ show answer

answer :: [String]

answer = do

a <- extend ""

b <- extend a

c <- extend b

return c

extend :: String -> [String]

extend string = (: string) <$> filter (`notElem` string) ['A'..'C']

use std::iter;

fn main(){

let answer: Vec<String> = answer();

println!("{:?}", answer);

}

fn answer() -> Vec<String> {

extend("")

.flat_map(|a| extend(&a).collect::<Vec<String>>())

.flat_map(|b| extend(&b).collect::<Vec<String>>())

.flat_map(|c| iter::once(c))

.collect()

}

fn extend<'a>(string: &'a str) -> impl Iterator<Item = String> + 'a {

('A'..='C')

.filter(move |c| string

.chars()

.all(|c_in_string| c_in_string != *c))

.map(move |c| format!("{}{}", c, string))

}

Haskell

Rust

41 of 42

RustをHaskellの観点から見てみる

  • Rustでモナドは作れるのか?

42 of 42

まとめ

  • cargo clippyコマンド
    • もっとイケてる書き方を提示してくれる
  • cargo docコマンド
    • ドキュメントを自動生成して可読性を高める
  • bitfield_structクレート
    • ビット単位の構造体を作れる
  • OnceCell
    • 一度だけ実行したい初期化処理
  • RustをHaskellの観点から見てみる
    • 文脈を隠蔽するdo記法みたいなのは標準機能としてはなさそう
    • しかしOptionやIteratorなど個別の文脈に>>=にあたるメソッドがあったりする