RubyのFiberで銀行口座を開設してみた

私は、Ruby の `Fiber` クラスをどう使うのかと今一つピンと来てませんでした。で、全然すっきりしていないのですが、考えているうちに、ふと、 Elixir の Process と似ているかも？ と気づきました。
ということで、 `Fiber` 初心者の私は、試しに、銀行口座を開設してみることにしました。

今回使っている Ruby のバージョンは、 2.7.0 です。

# 最初の実装

まずは、Fiber を使って以下のコードを書いてみました。

```ruby
account = Fiber.new do
  balance = 0
  loop do
    Fiber.yield balance
  end
end
```

ここで、何回、 `account.resume` を実行しても、 `Fiber.yield` の引数 `balance` の値が0であるため、結果は、0です。

```irb
irb(main):007:0> account.resume
=> 0
irb(main):008:0> account.resume
=> 0
irb(main):009:0> account.resume
=> 0
``` 

`Fiber.new` のブロックの中の `loop` の中を少しだけ変更してみます。

```ruby
account = Fiber.new do
  balance = 0
  loop do
    amount = Fiber.yield balance
    balance += amount || 0
  end
end
```

`account.resume` を呼び出す時に、引数に数値を指定して呼び出したり、引数なしで呼び出したりしてみます。

```irb
irb(main):009:0> account.resume # 最初の呼び出し
=> 0
irb(main):010:0> account.resume(1000) # 1000円の預入
=> 1000
irb(main):011:0> account.resume # 残高照会
=> 1000
irb(main):012:0> account.resume # 残高照会
=> 1000
irb(main):013:0> account.resume(-250) # 250円引き出し
=> 750
irb(main):014:0> account.resume # 残高照会
=> 750
irb(main):015:0> account.resume # 残高照会
=> 750
```

`resume` を呼び出す時に、残高照会なのか、引き出しなのか、預け入れなのか引数で指定して、明示的にわかるようにちょっとコードを書き換えてみます。

```ruby
account = Fiber.new do
  balance = 0
  loop do
    request, amount = Fiber.yield balance
    case request
    when :deposit
      balance += amount
    when :draw
      balance -= amount
    when :balance
      # do nothing
    end
  end
end
```

resume で呼び出す時は、 `:balance` (残高照会)、 `:deposit` （預入）、 `:draw` （引き出し）を指定するようにします。

```irb
irb(main):032:0> account.resume(:balance) # 残高照会 
=> 0
irb(main):033:0> account.resume(:deposit, 1000) # 1000円預入
=> 1000
irb(main):034:0> account.resume(:balance) # 残高照会
=> 1000
irb(main):035:0> account.resume(:draw, 250) # 250円引き出し
=> 750
irb(main):036:0> account.resume(:balance) # 残高照会
=> 750
```

銀行口座を複数作りやすいようにメソッド化します。

```ruby
def create_account
  Fiber.new do
    balance = 0
    loop do
      request, amount = Fiber.yield balance
      case request
      when :deposit
        balance += amount
      when :draw
        balance -= amount
      when :balance
        # do nothing
      end
    end
  end
end
```

２つの口座を開設してみます。

```irb
irb(main):017:0> account1 = create_account
irb(main):018:0> account2 = create_account
irb(main):019:0> account1.resume(:balance)
=> 0
irb(main):020:0> account2.resume(:balance)
=> 0
irb(main):021:0> account1.resume(:deposit, 3000)
=> 3000
irb(main):022:0> account2.resume(:deposit, 1200)
=> 1200
irb(main):023:0> account1.resume(:balance)
=> 3000
irb(main):024:0> account2.resume(:balance)
=> 1200
irb(main):025:0> account2.resume(:draw, 500)
=> 700
irb(main):026:0> account1.resume(:deposit, 750)
=> 3750
irb(main):027:0> account1.resume(:balance)
=> 3750
irb(main):028:0> account2.resume(:balance)
=> 700
```

口座振替（振込） ができるように、 `:transfer` の処理を create_account に追加します。

```ruby
def create_account
  Fiber.new do
    balance = 0
    loop do
      request, amount, other = Fiber.yield balance
      case request
      when :deposit
        balance += amount
      when :draw
        balance -= amount
      when :transfer
        balance -= amount
        other.resume(:deposit, amount)
      when :balance
        # do nothing
      end
    end
  end
end
```

（この辺、Ruby 2.7であれば、パターンマッチ使って、もう少しスマートなコードになりそうな気がしますが、今回は無視します。）

```irb
irb(main):048:0> account3 = create_account
irb(main):049:0> account4 = create_account
irb(main):050:0> account3.resume(:balance)
=> 0
irb(main):051:0> account4.resume(:balance)
=> 0
irb(main):052:0> account3.resume(:deposit, 5000) # account3 に 5000円預入
=> 5000
irb(main):053:0> account4.resume(:deposit, 1000) # account4 に 1000円預入
=> 1000
irb(main):054:0> account3.resume(:transfer, 1500, account4) # account3 から account 4 に1500円振り込み
=> 3500
irb(main):055:0> account3.resume(:balance) # account3 の残高
=> 3500
irb(main):056:0> account4.resume(:balance) # account4 の残高
=> 2500
```

Ruby っぽく Account をクラスにして、ラッピングしてみます。

```ruby
class Account
  def initialize
    @account = create_account
  end

  def balance
    @account.resume(:balance)
  end

  def deposit(amount)
    @account.resume(:deposit, amount)
  end

  def draw(amount)
    @account.resume(:draw, amount)
  end

  def transfer(amount, account)
    @account.resume(:transfer, amount, account.fiber)
  end

  protected

  def fiber
    @account
  end

  private

  def create_account
    Fiber.new do
      balance = 0
      loop do
        request, amount, other = Fiber.yield balance
        case request
        when :deposit
          balance += amount
        when :draw
          balance -= amount
        when :transfer
          balance -= amount
          other.resume(:deposit, amount)
        when :balance
          # do nothing
        end
      end
    end
  end
end
```

Accountクラスを使って口座を開設してみましょう。

```irb
irb(main):001:0> account1 = Account.new
irb(main):002:0> account2 = Account.new
irb(main):003:0> account1.balance
=> 0
irb(main):004:0> account2.balance
=> 0
irb(main):005:0> account1.deposit(5000)
=> 5000
irb(main):006:0> account2.deposit(1000)
=> 1000
irb(main):007:0> account1.draw(500)
=> 4500
irb(main):008:0> account1.transfer(1000, account2)
=> 3500
irb(main):009:0> account1.balance
=> 3500
irb(main):009:0> account2.balance
=> 2000
```

と、ここまで来て、わざわざ、 `Fiber` 使わんでも、 普通にインスタンス変数使って、 `Account` クラス作ればええやんと思ったあなたは、正しいです。 
次のような実装で同じようなことが実現できます。

```ruby
class Account
  attr_reader :balance

  def initialize
    @balance = 0
  end

  def deposit(amount)
    @balance += amount
  end

  def draw(amount)
    @balance -= amount
  end

  def transfer(amount, account)
    @balance -= amount
    account.deposit(amount)
  end
end
```

ということで、 `Fiber` 使わなくても、銀行口座を開設できるということがわかったのでした。言い方を変えれば、インスタンス変数もどきとして `Fiber` を利用できるということがわかったところでしょうか :sweat_smile: 

また、 `Fiber` に関して何か閃いたら、別の記事を書くかも知れません。

# 追記

最初に残高照会 `resume` を呼び出さないと `Fiber` の例はうまく動作しないことがわかりました。

```ruby
class Account
  def initialize
    @account = create_account
  end

  def balance
    @account.resume(:balance)
  end

  def deposit(amount)
    @account.resume(:deposit, amount)
  end

  def draw(amount)
    @account.resume(:draw, amount)
  end

  def transfer(amount, account)
    @account.resume(:transfer, amount, account.fiber)
  end

  protected

  def fiber
    @account
  end

  private

  def create_account
    Fiber.new do
      balance = 0
      loop do
        request, amount, other = Fiber.yield balance
        case request
        when :deposit
          balance += amount
        when :draw
          balance -= amount
        when :transfer
          balance -= amount
          other.resume(:deposit, amount)
        when :balance
          # do nothing
        end
      end
    end.tap(&:resume) # 1回 resume を呼び出す
  end
end
```

他にも、 `account.transfer(1000, account)` みたいに自分自身に振り込もうとすると、 `FiberError` になるとか、まあ、色々、改善の余地がありそうです :sweat_drops: 

# 参考情報

今回の記事を書くのにヒントとなった Elixir の書籍を紹介しておきます。 

* [Programming Elixir 1.6](https://pragprog.com/book/elixir16/programming-elixir-1-6) (Dave Thomas)