Help us understand the problem. What is going on with this article?

Rubyでもalgebraic effectsがしたい!

はじめに

これはRuby advent calendar 2019の7日目の記事です。

こんにちは、びしょ~じょです。
Ruby全然書かないけどふとした理由でRubyのライブラリを作りました。
それがこちら。
Nymphium/ruff: ONE-SHOT Algebraic Effects for Ruby!
このライブラリはone-shot algebraic effectsを提供します。

本記事では、このライブラリの使い方、競合ライブラリとの比較、なぜRubyで書いたかについて触れたいと思います。

Algebraic Effectsってなんだ

Algebraic Effects(またはAlgebraic Effects and Handlers, Algebraic effect Handlers, 和訳だと代数的効果(と勝手に筆者がつけてます))は最近流行りの言語機能です。
React Hooksの開発者のDan Abramovさんがツイッターやブログでalgebraic effectsについて触れているのを見たことがある人もいると思います。

機能的な側面で述べると、直感的には 継続を取得できる、復帰可能な例外およびハンドラ です。
Rubyにはcall/ccあったしRubyistの皆さんに継続の説明は不要ですね。
…すみません、しかし継続から説明するとだいぶ話が長くなるので、algebraic effectsの説明も兼ねて、手前味噌ですみませんがこちらのスライドを御覧ください。
0から知った気になるAlgebraic Effects - lilyum ensemble

せっかくQiita使ってるんで、Qiitaに投稿したこちらもどうぞ。
Algebraic Effectsとは? 出身は? 使い方は? その特徴とは? 調べてみました! - Qiita

play with Ruff

御託はOKなんで早速コードを見ていきましょう。
Ruff.instanceでエフェクトを生成し、effect.performでエフェクトを発生します
Ruff.handlerでハンドラを生成し、handler.on(effect)(&proc)でエフェクトeffectに対するハンドラを設定します。

require 'ruff'

Double = Ruff.instance

with_arith = Ruff.handler
                 .on(Double){|k, v| k[v * 2]}

with_puts = Ruff.handler
                .on(Double){|k, v| puts v; puts v; k[]}

with_arith.run {
  puts Double.perform 10 #==> 20
}

with_puts.run {
  Double.perform 10 #==> 10\n10
}

ウォーいい感じですね。
k は継続です。
他にも見てみますか。

ハンドラはhandler.to(&proc) というメソッドも持ち、ハンドルされているブロックが返す値をハンドルしてくれます。つまりvalue handlerを設定できます。
ログを収集するエフェクトとハンドラを定義してさっきのDoubleも混ぜてみます。

Log = Ruff.instance
# スマートコンストラクタ的な
log = ->(msg) { Log.perform msg }

log_collector = lambda {
  msgs = []
  Ruff.handler
      .on(Log) do |k, msg|
    msgs.push "log:#{msg}"
    k[]
  end
      .to do |x|
    [x, msgs]
  end
}

logs =
  log_collector.call.run {
  with_arith.run {
    log['hello']
    log['world']
    Double.perform 3
}}

puts logs
#==>
# 6
# log:hello
# log:world

ウォーいいですね。
numbered parameter があればもう少し良さそうですね。

これを使うといろいろ書けて(中略)良さげなエフェクト&ハンドラがruff/standardに定義されています。

例えばasync/awaitがあります。

require 'ruff/standard'

include Ruff::Standard

Async.with do
  task = lambda {|name|
    lambda {
      puts "Starting #{name}"
      v = (Random.rand * (10**3)).floor
      puts "Yielding #{name}"
      Async.yield
      puts "Eidnig #{name} with #{v}"

      v
    }
  }

  pa = Async.async task['a']
  pb = Async.async task['b']
  pc = Async.async lambda {
    Async.await(pa) + Async.await(pb)
  }

  puts "sum is #{Async.await pc}"
end
#==>
# Starting a
# Yielding a
# Eidnig a with 423
# Starting b
# Yielding b
# Eidnig b with 793
# sum is 1216

call/ccもあります!

Call1cc.context do
  divfail = lambda {|l, default|
    Call1cc.run {|k|
      l.map{|e|
        if e.zero?
          k[default]
        else
          e / 2
        end
      }
    }
  }

  pp divfail.call([1, 3, 5], [1]) # ==> [0, 1, 2]
  pp divfail.call([1, 0, 5], [1]) # ==> [1]
end

Rubyist歓喜…と言いたいところですが本ライブラリが提供するのはcall/1ccです。
(あとcall/1ccといっているがCall1cc.contextという範囲の中でのみ使えるので実際は限定継続です。ごめんね。)

競合ライブラリとの比較

rubygemウォッチャーならご存知かもしれませんが、Rubyにもalgebraic effectsのライブラリはすでに2つ存在します。

dry-effects

こちらはdry-rbというコミュニティの提供する1ライブラリのようです。
本ライブラリと比較してハンドラの定義がややデカいです。この辺は慣れなのであまり問題ではないかもしれません。
しかしdry-effectsは継続が使えないようです!
この点においては本ライブラリに軍配が上がりました。

affect

こちらは結構文法が似てますね。
しかしこちらも継続が使えません。
dry-effects同様、我々のほうが有利です。


継続が使えないと上記に定義したような AsyncCall1cc などが実装できません。
(継続を使わずに実装できるものはDIかなんかでも実装できるので、algebraic effectsライブラリと果たして言えるのか個人的には疑わしいですが、まあ何か思想があるのかもしれません。)

Why Ruby

再び我田引水で申し訳ないですが、こちらの方法を利用しています。
Asymmetric CoroutinesによるOneshot Algebraic Effectsの実装 - lilyum ensemble

簡単に述べると、コルーチンでalgebraic effectsが実装できます。
しかしこのとき、コルーチンの残りのスレッドが継続に対応し、コルーチンの状態はコピーできないので、継続はワンショットに制限されます。

ちょうど手頃に操作できるコルーチンを持っていたのがRubyだったのでとりあえず実装しておきました!!
実装内部を見てみるとパターンマッチなどが使われていてしんどかったッシュねえ…。
Rubyにパターンマッチが正式に追加されてもっと綺麗なコードベースになってるといいですねえ。

さらに、今回使ったコルーチンはasymmetric coroutineです。
簡単にいうと Fiber.yieldFiber.resume です。
symmetric coroutineを使った実装もちょっと考えてみたいので、そのときはモダンな言語の中でもsymmetric coroutine を持つ数少ない言語のRuby(Fiber.transfer)のお世話にまたなろうと思います。

おわりに

だいたい宣伝になってしまって申し訳ないですが、とにかくRubyでもワンショットのalgebraic effectsが使えます!
Rubyは謎構文もいっぱいありOOPとも協調していい感じにalgebraic effectsが埋め込めていて快適に書けます、最高

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away