専門の文法の支持の言語の中でmonadを使うのはみっともないです


先週JavaEyeクイズで見ました
論理演算の結果を求める、その中のDe Morganの法則の応用はnight_のようですstalker兄さんが言ったように、難しくはありません.しかし、このように法則を適用して推定するのは直感的ではないかもしれないので、その時私は角度を変えて、C#コードを書いて検証問題の中の表現式がDe Morgan法則を適用する前後の真偽値がいつも同じかどうかを窮挙したいと思っていました.
LINQでは、コードが簡潔です.
using System;
using System.Linq;

static class Program {
  static void Main(string[] args) {
    var booleanValues = new [] { true, false };
    var equal = !(from a1 in booleanValues
                  from a2 in booleanValues
                  from a3 in booleanValues
                  from b1 in booleanValues
                  from b2 in booleanValues
                  from b3 in booleanValues
                  select (!((a1 && b1) || (a2 && b2) || (a3 && b3))) ==
                         (!(a1 && b1) && !(a2 && b2) && !(a3 && b3)))
                  .Contains(false);
    Console.WriteLine(equal); // true
  }
}

このコードは比較的直感的に見えるはずです.でも...あまり直観的じゃないみたい?
多くの人にとって、ループを明示的に使うとより直感的になるかもしれません.
using System;

static class Program {
  static void Main(string[] args) {
    var booleanValues = new [] { true, false };
    var equal = true;
    foreach (var a1 in booleanValues) {
      foreach (var a2 in booleanValues) {
        foreach (var a3 in booleanValues) {
          foreach (var b1 in booleanValues) {
            foreach (var b2 in booleanValues) {
              foreach (var b3 in booleanValues) {
                if ((!((a1 && b1) || (a2 && b2) || (a3 && b3))) !=
                    (!(a1 && b1) && !(a2 && b2) && !(a3 && b3))) {
                  equal = false;
                  break;
                }
              }
              if (!equal) break;
            }
            if (!equal) break;
          }
          if (!equal) break;
        }
        if (!equal) break;
      }
      if (!equal) break;
    }
    Console.WriteLine(equal); // true
  }
}

でもこのコードは醜いですね=v=
カンニングをしました.以上の2つのバージョンのコードを比較すると、実際の実行プロセスは異なります.LINQのバージョンはすべての可能性を計算してからContains()かどうかを見ます.Containsはeager operatorで、Select、Whereのようにlazyではありません.ループ版は、等しくない状況に遭遇するとそのままループを終了します.しかし、いずれにしても2つのバージョンの計算結果は同じで、比較に使っても過言ではありません.
実はC#のforeachサイクルは多くの細部を隠しており、明示的に下付きまたはIEnumeratorで容器を巡る必要はありません.しかし、私はやはりこのようなシーンで明示的なループを使うのが好きではありません.主にループがネストされているのがとても見苦しいので、うまくいかないとスクリーンの右側を超えます.
前にLINQのバージョンは簡潔に見えるが、必ずしも直観的ではないのは、複数のfrom句がつながっている対応する関数がSelectManyであり、IEnumerable/IQueryableにとってこのLINQ演算子の背後にある概念はlist monadのbind関数であるからである.そのメカニズムはやはり資料を読んでこそ理解できる.monadという言葉を見ると気絶する人も多いのではないでしょうか.=w=
本当はlist monadをC#でLINQで使ってこんなに簡潔にできると思っていたのですが、ルビーでもそう使えたらいいなと思いました.しかし、タイトルのように、言語がmonad文法を提供していない場合、この遊びは書くのも優雅ではありません.実はもっと重要な問題はtypeclassがない条件の下でmonadは抽象的な階層を高めるために統一的なインタフェースを提供できないかもしれませんか?しかし、私は文法、つまり「どう見えるか」の問題に関心を持ちたいだけです.
例えば、Rubyでmaybe monadを実現します.
class Maybe
  def initialize(val, has_val = true)
    @value, @has_value = val, has_val
  end
  
  def value
    raise 'Maybe::None has no value' unless @has_value
    @value
  end
  
  class << self
    def unit(val)
      new(val).freeze
    end
  end
  
  def bind
    @has_value ? (yield @value) : Maybe::None
  end
  
  def none?
    self == Maybe::None
  end
  
  def clone
    raise TypeError, "can't clone instance of Maybe"
  end
  
  def dup
    raise TypeError, "can't dup instance of Maybe"
  end
  
  None = Maybe.new(nil, false).freeze
  
  private_class_method :new, :allocate
end

Maybeクラスを定義したら、次のように使用できます.
res1 = Maybe.unit(1).
             bind { |x| Maybe.unit(x + x) }.
             bind { |x| Maybe.unit(x * x) }
#=> #<Maybe:0x34bc3e8 @has_value=true, @value=4>

見た目は悪くないですが、メソッド呼び出しはネストされておらず、つながっています.
しかし、これはこの一連の計算が単一のソースからデータを得たからにすぎない:その「1」.1つの「1」と1つの「2」をMaybeで包装し、それらの和を得るには、次のようになります.
one, two = [1, 2].map { |i| Maybe.unit i }
res2 = one.bind { |x|
         two.bind { |y|
           Maybe.unit(x + y)
         }
       }
#=> #<Maybe:0x35ece5c @has_value=true, @value=3>

そこでbindの呼び出しはネストして、気がふさぎますT
Haskellに書いた時にdoを使わなくてもそんなに面倒ではなかったのを覚えているのに、こんな感じでした.
Just 1 >>= \x -> Just 2 >>= \y -> return (x + y)

そして思い出したのは実はこの式がネストされたOTL
(Just 1) >>= (\x -> ((Just 2) >>= (\y -> (return (x + y)))))

やはりカッコがなくて見やすいように見えるのですが…優先度が違うので、Haskellで省略できるカッコはRubyでは省略できません.おとなしく書くしかないでしょう.
まぁ、いずれにしても、Maybeクラスの定義があれば、Maybe::Noneを伝える能力を得ました.前の1+2を1+Noneに変えるように、得られるのはNoneです.
res3 = one.bind { |x|
         Maybe::None.bind { |y|
           Maybe.unit(x + y)
         }
       }
#=> #<Maybe:0x316384 @has_value=false, @value=nil>
res3.none?
#=> true

それとも一行に書きますか?
res3 = one.bind { |x| Maybe::None.bind { |y| Maybe.unit(x + y) } }

Haskellで書くと、do記法を使わずに
Just 1 >>= \x -> Nothing >>= \y -> return (x + y)

ドで覚えると
do x <- Just 1
   y <- Nothing
   return (x + y)

ええと、maybe monadを見たことがありますが、list monadは?
Rubyの中のEnumerableモジュールは実は1つの抽象的な表と見なすことができて、中のselect、inject、map/collectなどの方法はすべて関数式のプログラミングの対応物があります:filter、fold_left、mapなど.ではlist monadのbind関数をEnumerableに書いておきましょう.
module Enumerable
  def bind
    self.inject([]) { |acc, e| acc + (yield e) }
  end
end

次に、例えば、[1,2]の各要素と[3,4,5]の各要素との積のリストを要求する.
[1, 2].bind { |x|
  [3, 4, 5].bind { |y|
    [x * y]
  }
}
#=> [3, 4, 5, 6, 8, 10]

うーん、思ったほどきれいじゃないT T
リストmonadを使わなくても、eachで直接書くのは悪くないようです.
res = []
[1, 2].each { |x|
  [3, 4, 5].each { |y|
    res << (x * y)
  }
}
#=> res == [3, 4, 5, 6, 8, 10]

専門的な文法サポートのある言語では、この論理は優雅に書かれています.
do x <- [1, 2]
   y <- [3, 4, 5]
   return (x * y)
-- [3,4,5,6,8,10]
from x in new [] { 1, 2 }
from y in new [] { 3, 4, 5 }
select x * y
//=> { 3, 4, 5, 6, 8, 10 }

Haskellではdo記法を使わなくても見苦しくありません:
[1, 2] >>= \x -> [3, 4, 5] >>= \y -> return (x * y)

冒頭のC#の例に戻り、上のRubyのリストmonadで書くと、
boolVals.bind { |a1|
  boolVals.bind { |a2|
    boolVals.bind { |a3|
      boolVals.bind { |b1|
        boolVals.bind { |b2|
          boolVals.bind { |b3|
            [(!((a1 && b1) || (a2 && b2) || (a3 && b3))) ==
            (!(a1 && b1) && !(a2 && b2) && !(a3 && b3))]
          }
        }
      }
    }
  }
}.all? { |b| b }
#=> true

微妙な感じ・・・
このmonadをルビーでもっときれいにする方法はありませんか?それとも私が要求しすぎたの?
LINQのSelectManyのようにBindを1つのパラメータを多く受け入れるバージョンにしても、呼び出しはネストされるでしょう(lambda式はネストする必要はありませんが).カッコを少なくしてインデントを少なくしたいだけですが...
週末に『The Ruby Programming Language』を読んでインスピレーションを探してみましょう.
ところで、リンクをメモします.
MenTaLguY: Monads in Ruby
まだ読んでいませんが・・・