🦀
Ruby のコードで理解する Rust の Result の振る舞い
はじめに
Rust の unwrap unwrap_or expect の仕組みや考え方を理解するために Ruby で似たようなものを実装してみる。
抽象化したコード
class Result
def initialize(value)
@value = value
end
def inspect
"#{self.class.name}(#{@value.inspect})"
end
end
class Ok < Result
def unwrap
@value
end
def unwrap_or(iferr)
unwrap
end
def expect(message)
unwrap
end
def map(&block)
self.class.new(block[@value])
end
end
class Err < Result
def unwrap
raise "panicked: #{inspect}"
end
def unwrap_or(iferr)
iferr
end
def expect(message)
raise message
end
def map(&block)
self
end
end
def Ok(value)
Ok.new(value)
end
def Err(value)
Err.new(value)
end
基本動作
- Err に対して値を取り出そうとするとエラーになる
- ポリモルフィックな操作ができたりする
x = Ok(200) # => Ok(200)
x.unwrap rescue $! # => 200
x.unwrap_or(300) rescue $! # => 200
x.map { |e| e * 2 } # => Ok(400)
x.expect("失敗") rescue $! # => 200
x = Err(500) # => Err(500)
x.unwrap rescue $! # => #<RuntimeError: panicked: Err(500)>
x.unwrap_or(300) rescue $! # => 300
x.map { |e| e * 2 } # => Err(500)
x.expect("失敗") rescue $! # => #<RuntimeError: 失敗>
Ruby 的には仰々しく強引だがデザインパターンの一つとして有用かもしれない。
Rust は配列が Result 型に依存したメソッドを持っている?
Rust の配列は要素が Result 型であることを想定したメソッドを持っているように見える。
Itertools は外部のライブラリだけど AtCoder でも使えるぐらい標準よりのライブラリで、それが提供している配列のメソッド map_ok は要素が Result 型だと想定している。
Rust
use itertools::Itertools;
fn main() {
let v = vec![Ok(5), Err("x"), Ok(6), Err("x")];
let v = v.into_iter().map_ok(|e| e * 2).collect_vec();
println!("{:?}", v); // >> [Ok(10), Err("x"), Ok(12), Err("x")]
}
Ruby でも書くとかなり抵抗がある。map なんてのはすべてのオブジェクトが持っているメソッドではないのだから。
module Enumerable
def map_ok(&block)
collect { |e| e.map(&block) }
end
end
v = [Ok(5), Err("x"), Ok(6), Err("x")]
v.map_ok { |e| e * 2 } # => [Ok(10), Err("x"), Ok(12), Err("x")]
ここで試しに Rust のコードの配列要素を整数にすると、なんと map_ok が使えなくなった。というかまずコンパイルが通らなくなった。つまり map_ok は Result 型要素の配列にしか生えていない。Rust の配列は要素の型ごとにもっているメソッドが異なる。おそらく map_ok メソッドを持ったトレイトの適用範囲が異なるんだろう。
if let Ok(x) = xxx
を Ruby で書けるか?
Rust の ラップされたままポリモルフィックであれこれできるとはいえ結局パニックを避けつつ値を取り出して分岐するコードを書くことになる。このとき Rust にはスマートな構文がある。
Rust
let v: Result<_, &str> = Ok(200);
if let Ok(x) = v {
println!("{:?}", x); // >> 200
}
これを Ruby で普通に書くと v.kind_of?(Ok)
なら x = v.unwrap
となって冗長なのでパターンマッチングを使ってみる。
class Result
def to_h
{ self.class.name.to_sym => @value }
end
end
v = Ok(200)
if v.to_h in { Ok: x }
x # => 200
end
どうだろう? Ok をシンボルにしないといけないので、それに合わせて to_h が必要になってきていまいちか。
Discussion