🦀

Ruby のコードで理解する Rust の Result の振る舞い

2022/03/25に公開

はじめに

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 メソッドを持ったトレイトの適用範囲が異なるんだろう。

Rust の if let Ok(x) = xxx を Ruby で書けるか?

ラップされたままポリモルフィックであれこれできるとはいえ結局パニックを避けつつ値を取り出して分岐するコードを書くことになる。このとき 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