私は、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
を利用できるということがわかったところでしょうか
また、 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
になるとか、まあ、色々、改善の余地がありそうです
参考情報
今回の記事を書くのにヒントとなった Elixir の書籍を紹介しておきます。
- Programming Elixir 1.6 (Dave Thomas)