Septeni Engineer's Blog

セプテーニエンジニアが綴る技術ブログ

C#とclojureのコードを比較してみた

こんにちはkouです。
前回の記事で書いたC#のコードをclojureで書いてみて比較してみようという誰が得するのかよくわからない記事です。
ただ私がclojureを書きたかっただけの記事です。

clojureをご存知でない方も多いかと思いますが簡単に説明すると、JVM上で動作するlisp系の言語です。


弊社ではScalaを利用したプロジェクトがどんどん増えているなか、なぜclojureの記事なんて書いてるんだとか社内の人間に思われそうですが、あれです多様性です、diversityです。

早速二重ループの処理を比較してみようかと思います。

        public static void TestLinq()
        {
            List<string> testList = new List<string>();
            testList.Add("test");
            testList.Add("sample");
            testList.Add("hoge");
            testList.Add("ex");
            testList.Add("etc");

            foreach (var test in testList)
            {
                foreach (var i in Enumerable.Range(1, 5))
                {
                    System.Console.WriteLine("{0} : {1}", test, i);
                }
            }
        }
(def tl `("test" "sample" "hoge" "ex" "etc"))

(defn test-clj [test-list]
  (for [test test-list
        i (range 1 5)]
    (str test " : " i)))

(test-clj tl)


Listの持つ場所や関数の引数、各種命名規則的なのは変えてます。
clojureのforは多重ループになってもどんどん追加するだけでいいので、ネストしないで書けるのがいい感じですね。
形としてはクエリ式で作ったやつとほとんど同じです。
あと本筋とは関係ないですがシンタックスハイライトやらが微妙ですね…。

こちらがクエリ式ver

        public static void TestLinq2()
        {
            List<string> testList = new List<string>();
            testList.Add("test");
            testList.Add("sample");
            testList.Add("hoge");
            testList.Add("ex");
            testList.Add("etc");

            var resultStrs = from test in testList
                             from i in Enumerable.Range(1, 5)
                             select new { test, i };

            resultStrs.ToList().ForEach(System.Console.WriteLine);
        }


次はループの最中に処理があるやつです。

        public static void TestLinqEx()
        {
            List<string> testList = new List<string>();
            testList.Add("test");
            testList.Add("sample");
            testList.Add("hoge");
            testList.Add("ex");
            testList.Add("etc");

            foreach (var test in testList)
            {
                var addStr = test + " new version";
                foreach (var i in Enumerable.Range(1, 5))
                {
                    var addStr2 = test + ":" + i.ToString();
                    System.Console.WriteLine("{0} : {1}", addStr, addStr2);
                }
            }
        }
(def tl `("test" "sample" "hoge" "ex" "etc"))

(defn test-linq-ex [test-list]
  (for [test test-list]
    (let [add-str (str test " new version")]
      (for [i (range 1 5)]
        (let [add-str2 (str test ":" i)]
          (str add-str " : " add-str2))))))

(test-linq-ex tl)

(defn test-linq-ex2 [test-list]
  (for [test test-list
        :let [add-str (str test " new version")]
        i (range 1 5)
        :let [add-str2 (str test ":" i)]]
    (str add-str " : " add-str2)))

(test-linq-ex2 tl)


二つ書いてみました。test-linq-exの方はまんま書いてみました。ローカル変数定義でネストされるので、なんか余計にネストされてて気持ち悪いですね。
普通に書いたのがtest-linq-ex2の方です。こっちはforで全部まとまってます。こちらもクエリ式で作ったやつと形が同じ感じですね。

        public static void TestLinqEx2()
        {
            List<string> testList = new List<string>();
            testList.Add("test");
            testList.Add("sample");
            testList.Add("hoge");
            testList.Add("ex");
            testList.Add("etc");

            var resultStrs = from test in testList
                             let addStr = test + " new version"
                             from i in Enumerable.Range(1, 5)
                             let addStr2 = test + ":" + i.ToString()
                             select new { addStr, addStr2 };

            foreach (var resultStr in resultStrs)
                System.Console.WriteLine("{0} : {1}", resultStr.addStr, resultStr.addStr2);
        }


次はsumのオーバーロードについてのコードです

        public static void TestSum()
        {
            List<int> priceList = new List<int>();
            priceList.Add(1000);
            priceList.Add(2000);
            priceList.Add(5000);
            priceList.Add(10000);

            var results = from price in priceList
                          from category in Enumerable.Range(1, 5)
                          select new { price, category };

            // selectいらない
            //var sum = results.Where(x => x.category == 3).Select(x => x.price).Sum();
            var sum = results.Where(x => x.category == 3).Sum(x => x.price);
            System.Console.WriteLine(sum);
        }
(def pl
  (let[price `(1000 2000 5000 10000)]
    (for [p price
          category (range 1 5)]
      {:price p :category category})))

(defn test-sum [price-list]
  (reduce +
          (map :price
               (filter #(= (:category %) 3) price-list))))

(test-sum pl)


まんま書くとこんな感じです。

クエリ式に関してはplの部分で処理しています。リストの作成方法がちょいと違うくらいでしょうか。

linqのsumにあたるものはclojureでは見つからなかったのでこんな感じになってます。
linqの方はメソッドチェーンで読みやすいのですが、
clojureの方はfliter,map,reduceの流れが下から上になっていてとても読みづらいです。
こういう時はスレッディングマクロというのを使うといい感じ書けたりします。

(def pl
  (let[price `(1000 2000 5000 10000)]
    (for [p price
          category (range 1 5)]
      {:price p :category category})))

(defn test-sum2 [price-list]
  (->> price-list
       (filter #(= (:category %) 3))
       (map :price)
       (reduce +)))

(test-sum2 pl)


price-listに対してfilter,map,reduceしてるのが一気に分かりやすくなったと思います。
スレッディングマクロを使うことで上から下に素直に読み進める感じになってますね。

お次はwhereのオーバーロードについてのコードです

        public static void TestFirst()
        {
            List<int> priceList = new List<int>();
            priceList.Add(1000);
            priceList.Add(2000);
            priceList.Add(5000);
            priceList.Add(10000);

            var results = from price in priceList
                          from category in Enumerable.Range(1, 5)
                          select new { price, category };

            // whereいらない
            //var firstPrice = results.Where(x => x.category == 3).Select(x => x.price).First().price;
            var firstPrice = results.First(x => x.category == 3).price;
            System.Console.WriteLine(firstPrice);
        }
(def pl
  (let[price `(1000 2000 5000 10000)]
    (for [p price
          category (range 1 5)]
      {:price p :category category})))

(defn test-first [price-list]
  (->> price-list
       (filter #(= (:category %) 3))
       (map :price)
       (first)))

(test-first pl)


linqのような便利なオーバーロードは見つけられなかったので普通にfilter,map,firstしています。

最後にループのインデックスの取得についてのコードです

        public static void TestIndex()
        {
            List<string> testList = new List<string>();
            testList.Add("test");
            testList.Add("sample");
            testList.Add("hoge");
            testList.Add("ex");
            testList.Add("etc");

            foreach (var test in testList.Select((x, i) => new { x, i }))
                System.Console.WriteLine("{0} : {1}", test.x, test.i);
        }
(def sl `("test" "sample" "hoge" "ex" "etc"))

(defn test-index [str-list]
  (map-indexed list str-list))

(test-index sl)


map-indexedという関数で簡単にインデックスを付ける事ができます。
こっちはclojureのがシンプルな感じに見えます。

っとこんな感じで比較してきました。
多分多くの方は()がなぁ…という感想だと思いますが、()は気にせずインデントのみを気にして見てもらえればいいかなぁと思ってます。