1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-10

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

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

最初の実装

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

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

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

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

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

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

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

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

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(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

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

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

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

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 に追加します。

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(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 をクラスにして、ラッピングしてみます。

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(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 クラス作ればええやんと思ったあなたは、正しいです。
次のような実装で同じようなことが実現できます。

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 の例はうまく動作しないことがわかりました。

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 の書籍を紹介しておきます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?