ラベル functional programming の投稿を表示しています。 すべての投稿を表示
ラベル functional programming の投稿を表示しています。 すべての投稿を表示

2019年2月25日月曜日

「データと振る舞いの一体化」と擬人化の弊害

最近 Functional Programming for the Dysfunctional という、面白い関数型プログラミングの紹介記事を読んだ。

序盤あたりに OOP と FP の対比があるが、ざっと要約すると下のようになる。

OOPは WHAT=データ, HOW=振る舞い, WHEN=時間を編み合わせて新たな複雑性を生み出してしまうが、逆に FPはこれらを分離してシンプルにする。

この説によれば、よく言われるような OOP と FP との直交性どころか、二つは相反する真逆のアプローチってことになるけど、オブジェクト指向から純粋関数型に移ってきた人なら、結構、実感としてわかるんじゃないだろうか。

さらに OOP に着目して少し敷衍すると、低結合・高凝集を目指してたはずの OOP 自らが、結合度を高め、凝集性を低める元凶になってしまっているとも言えないこともない。OOPやってた頃は考えもしなかったが、少なくとも今はそのように理解している。

ところで、ここで二つの疑問が湧いてくる。一つは、どうしてデータと振る舞いの一体化なんて愚行が、OOPの現場で未だに行われ続けてるのだろうって疑問(発祥はともかく)。もう一つは、どうしてこの手法の拙さが推奨されたまま疑問視されないのかって疑問。

■ なぜデータと振る舞いの一体化を止めないか

Simple Made Easy という、Clojure の開発者 Rich Hickey 氏が Simple と Easy の違いを解説した有名なプレゼンがあるが、その中で OOP では Simple と Easy を混同した挙句、初期のハードルが高めの Simple を避けて Easy に走ってしまいがちなことが詳説されている。

つまり「データと振る舞いを一体化」も、例のごとくOOP技術者が易きに流れてるだけって話ではないだろうか。OOP界隈では DB と HTTP の間の全てを巨大な手続きに混ぜ込んだトランザクションスクリプトがよく批判されるけど、OOP自身もせいぜい「データと振る舞いの一体化」に留まった中途半端なレベルから出ていなかったのではないかと。

実際に、そのレベルから脱却した純粋関数型DDD の近年の発展を見ていると、そのように思えてしまう。

■ なぜデータと振る舞いの一体化に疑問を持たないのか

最初に紹介した記事では、OOP と anthropomorphized=擬人化の関連も指摘されている。この擬人化には、協働(collaborationDictionary)と共に責務(responsibility)も含まれるけど、OOP界隈ではこれが特に重視されている。

まあ「どこに何を置くか」考えるのは大事だし、それを主体性や自意識をもった何かのごとく責務と呼ぶことは別に良しとしよう。

ただ困ったことに、これが神聖視されすぎたまま魔法のワードというか免罪符になってしまって、それでデータと振る舞いの一体化なんてよく考えたら変なことまでも、なんか責務!って言っちゃうとそれで正しいような錯覚が生まれてしまうのではなかろうかと思う。


まあ自分も、キャリアの大半をオブジェクト指向でやってきたので、ついつい責務って言葉を頻用してしまうけど、関数型プログラミングの現場だとやっぱそろそろ控えた方が良いかもしれない。恥ずかしいしな。

2019年2月11日月曜日

オブジェクト指向エクササイズの陳腐化

オブジェクト指向エクササイズというのが一頃流行った。自分も Java プログラマだったころは、1,000〜2,000行ほどのプログラムに適用して試したり、同僚にも勧めたりしたことがある。

ただ今の時代、パラダイム面でオブジェクト指向一強だった昔とは違うし、自分でもフルタイムで関数型プログラミングをやるようになったりして、改めて見直してみるとオブジェクト指向エクササイズも色々と怪しいところが多い。

この記事では関数型の視点も取り入れつつ再評価してみたい。

■ 評価の観点
オブジェクト指向エクササイズは『ThoughtWorks アンソロジー』所収のエッセイで紹介されたもので、著者によればオブジェクト指向の考え方を磨くのに役立つという9つのルールから成る。

原語だと Object Calisthenics と言うが、Calisthenics とは「懸垂、腕立て伏せ、ストレッチ、ジャンピングジャックのような、健康状態や体型を維持・改善するための器具を使わない運動(Vocaburary.com)」の事を指す。カタカナでエクササイズというと練習問題のような語感もあるが、もともとは体育・スポーツ・健康管理に関連する言葉らしい。

メタファーの背景にこうした語義があることを踏まえつつ、次の4段階で9つのルールを見直してみる。
  • 有益: 練習効果だけではなく、実プロジェクトでもルールやガイドラインとして取り入れてみる価値のあるもの。野球の素振りや格闘技の技の練習など、本番で確実に再現するための反復練習にも似たもの。
  • 練習としては有効: 上述の Calithenics の語義にまさに近く、著者が意図している通りのもの。実戦でそのまま使う所作でははないが、基礎体力育成のための筋トレやストレッチのような効果が期待できるエクササイズ。
  • ほぼ無意味: あえて実施してみたところで、せいぜい、やはり意味はなかったと納得するしかなさそうなルール。時間の無駄なので、なにか他の勉強でもしていた方が吉なもの。
  • 有害: むかーしの運動部では、ウサギ跳びや練習中の水飲み禁止など、今では医学的に否定されている練習や慣行が奨励されていた。それらと同じように効果が疑わしいどころか、むしろ悪い癖がついて不健康にすらなりそうなプラクティス。
以下、それぞれ見てみる。


■ 1. Only One Level Of Indentation Per Method
〜 一つのメソッドにつきインデントは1段階までにすること 〜

これは有益で、実プロジェクトでもある程度取り入れる価値がある。

エッセイでは SRP(単一責任原則)や短いメソッド(3行のメソッド)に関連して解説されているが、実は SLAP(Single Level of Abstraction Principles)にも関係が深い。

Kent Beck が 『Smalltalk Best Practice Patterns』で紹介していたComposed Methodにも関連するが、抽象度を適切にそろえて構成したメソッドは、5行前後の1段階のコードブロックで表現できるというもの。

これはオブジェクト指向ではなくても言えることで、Scala だと特に、for comprehension のネストは、抽象レベルが混在している可能性が高いので普段から避けたい。


■ 2. Don’t Use The ELSE Keyword
〜 ELSE 句を使用しないこと 〜
これはほぼ無意味

たとえば、下の式は連続一様分布の関数を普通の数式として表したものだけど、これを自然にコーディングすると当然 ELSE 句が生じる。これを ELSE句を使わずに書いてもかえって不自然すぎというか間違ってるので練習としての意味もない。
\[y = \begin{cases} \frac{1}{b - a}, & \text{for } x \in [a, b] \\ 0, & \text{otherwise} \end{cases} \]
また、エッセイでは IF 〜 ELSE を三項演算子で置き換えているサンプルもあるが、三項演算子は式だから OKということなら Scala で IF 文を式として扱えば、ELSE 句を禁止できないことになり、そもそもの基準があやふやになる。

あと、いつものオブジェクト指向のアレで、やたらとポリモーフィズムを連呼して Strategy パターンや NullObject パターンを推奨しているが、せっかく代数的データ型とパターンマッチで書いたシンプルなコードを、ムダな間接層を伴うパターンで置き換えてもまともな知恵は何一つ得られない。やってみてがっかりしても害にはならないが時間の無駄。


■ 3. Wrap All Primitives And Strings
〜 すべてのプリミティブ型とストリング型をラップすること 〜

これは有益。エクササイズに留めず、実戦のコーディングやレビュー観点に取り入れてみる価値があると思う。

もっと言えば、昔の Java ではラップするだけで精一杯だったかもしれないが、現代の Scala プログラミングではジェネリクスや型レベルプログラミングや依存型など、のパワーを追求する道具立てが昔より豊富なので、どんどん活用すると良いと思う。


■ 4. First Class Collections
〜 ファーストクラスコレクションを使用すること 〜

これはほぼ無意味。オブジェクト指向では List は準プリミティブなコンテナオブジェクトに過ぎないのだろうけど、関数型ではあえてドメイン固有の要素から切り離した普遍的な代数的性質なのだから(内部実装はともかく)、せっかく導出した抽象構造をラップして元に戻すのは色々逆行することになり不毛。

また「0または1以上」の多重度をファーストクラスコレクションにするなら、「0 または 1」でも同じことが成り立つはずで、だとすると Option もダメってことになるが、Option[Name] を MaybeName にラップする不毛さはすぐ分かると思う。さらに関数の定義域や値域に使うのもダメとなれば、自然なパターンマッチすらもできなくなる(無理して unapply を書けなくもないが無意味)し、言語が提供する利点を無駄に捨てているだけで、練習としての効果もない。

普通に Option、List、NonEmptyList(Catsなどの)、Sized(Shapelessの) などで表現した方が端的に表現できるデータの性質を、オブジェクト指向ではどうだからといった理由だけでむやみにラップしても何も良いことがない。

カプセル化の観点で見ても、参照透過でイミュータブルなコードを書いていれば、コレクション内部の値を外から勝手に変更することはできず「隠蔽」の価値は半減するので、無駄なラッパー層のコストに釣り合うメリットはやはり無い。


■ 5. One Dot Per Line
〜 1行につきドットは1つまでにすること 〜

ドットの連鎖で生じるいくつかのオブジェクトの責務を、「仲介者」が持ってしまうことになる状況を、単一責任原則デメテルの法則に則って防ぎたい、あるいはその発想を身にけたいって主旨だが、、、ほぼ無意味

ドットの連鎖禁止でできなくなることには、以下のようなものがあるが、、、
  • List や Stream を、map して filter して reduce するような計算
  • DDD でいう Closure of Operations (例えば数値計算の演算子もメソッド呼び出しだと考えれば、a + b はOKでも a + b + c は a.plus(b).plus(c) だからアウト。三角形の面積すら計算できない。)
  • 関数や計算の合成
  • fluent interface な EDSL
「オモチャのオモチャ」で遊ぶのをやめさせるための縛りとしてドットの連鎖に着目するのは、普及期ごろの Java ではそれで良かったかもしれないが、昨今のプログラミング技法を鑑みると時代遅れだし、ましてや関数型プログラミングでは的ハズレすぎてなんの学びも得られない。

※ドットで改行しても同じことだから、「一行あたりのドット」というよりドットの連鎖の禁止として考えている。


■ 6. Don’t Abbreviate
〜 名前を省略しないこと 〜

クラス名、メソッド名、フィールド名を省略しないことには別に異存はない。というか別にあえて禁止するまでもなく普通そうなのだから、練習としては毒にも薬にもならない。ただし逆にローカル変数の命名では、一文字に省略しても可読性が落ちないようにスコープを工夫したほうが良いので、むしろ有害の可能性もある。

つまり省略により可読性が落ちるしたら、関数が大きすぎて今読んでいる行とローカル変数の宣言行が離れすぎているために型が視界の外に出てしまっているとか、あるいは変数が多すぎて紛らわしくなっているとかなので、スコープをコンパクトにすれば済む。むしろそうする方が、名前をフルスペルにする事なんかより遥かにコード改善効果がある。

そもそも本当に命名の省略が常に可読性を劣化させるなら、数学や物理の公式もフルスペルの変数や記号で書かれてるはずだろうが、そうはなってないし、それどころか全部フルスペルにしたら逆に可読性が落ちて読みにくいことくらい少し想像すればわかる。

さらに関数型だと抽象度が高いコードも増えて、具体的な名前をつけようがない f、g、h としか呼べない関数を操作することが頻繁にある。ジェネリクスの型パラメータの名前だって、フルスペルにしたら逆に読みにくい。

大昔は「1文字変数名はループの i と j のみしか許さない」みたいなルールがあったものだけど、本当はクソ長いコードでループや条件ブロックを入れ子にしてるから可読性が落ちてただけで、直す場所が違う。


■ 7. Keep All Entities Small
〜 全てのエンティティを小さくすること 〜

これは有益。プログラムの構成要素について「いくら何でもこれ以上大きくなったら異常」という上限は、「Rule of 30」あたりを目安にすれば良いだろうけど、ちょうどよい小ささの発見は対象とスキル次第になる。

ただし Scala で上手く関数型プログラミングすれば、従来の Java よりは小さくなることはほぼ確実なので、『FP in Scala』などを参考にしながら、オブジェクト指向エクササイズというより関数型エクササイズとして練習すれば良いと思う。


■ 8. No Classes With More Than Two Instance Variables
〜 一つのクラスにつきインスタンス変数は2つまでにすること 〜

実プロジェクトのルールには使えないが練習としては有効かもしれない。

このルールを適用すると全ての複合型を2分木状の型構成で表現することになるけど、3要素以上で構成するのが自然な型も普通に多い。たとえば、AWS EC2 の `describe instances` コマンドの Output を2分木で表現することを想像すると、ひどく不自然になるのはすぐ分かる。だから 2という縛りは飽くまでトレーニング用の人工的な制約でしかない。

とはいえ、スポーツなどでも実戦にはない負荷をかけたり人工的に作った状況下での練習に効果があるように、このエクササイズも一度やってみる価値はあるかもしれない。ちなみに関数型な Scala で練習するとしたら、case class 等の Product Type だけではなく、sealed trait と case object で構成した Sum Type にもこのルールを適用すると、良いモデリング練習になると思う。


■ 9. No Getters/Setters/Properties
〜 Getter/Setter/プロパティを使用しないこと 〜

ほぼ無意味。代数的データ型の Product Type に「Tell, Don't Ask」を適用するのはナンセンス。むしろ無駄な責務の肥大化を招くだけ。

例えば、エッセイに Name クラスがサンプルとして載っているが、これを文字列に書式化したり Json に変換したりする場合、メソッドを Name に持たせるのは責務の置き場所が違う。`Visitor` パターンなどを使えばできるかもしれないが、コスト以上のメリットも得られず無駄な複雑さを招くだけ。

また例えば、数値と単位をそれぞれフィールドとしてもつ体重クラスと身長クラスから BMI を求めるコードを考えると、フィールドにアクセスできない以上クラスの外では計算できないし、かといってBMIの計算は体重の責務でも身長の責務でもないし、すぐに詰んでしまう。

「Tell, Don't Ask」を適用できるある種の文脈もないことはないが(特にOOPでは)、これを一般化してプログラム全体に適用するには無理がある。ましてや関数型プログラミングの場面では、よほど暇ならばあえて実験してみてやはり意味無しという結果を得るのも一興かもしれないけど、不自然なプログラムを書く練習にしかならないから不毛。


■ 結論
関数型からみた9つのルールの再評価結果
  • 有益: 1, 3, 7 の 3個
  • 練習としては有効: 8 の 1個
  • ほぼ無意味: 2, 4, 5, 9 の 4個
  • 有害: 6 の 1個
「無意味」と「有害」で過半数。残った「有益」と「有効」も、別にオブジェクト指向に限った話ではなく、「プログラミングエクササイズ」でも「関数型エクササイズ」でも成り立つし、なんなら「ベターな手続き型プログラムを書くための練習ルール」としても採用できる普通の話だった。まあ Java とか、"Better Java" 程度の Scalaプログラミングで精一杯の感じのあれなら、やってみても良いかもしれない。

■ 補足
オブジェクト指向について
オブジェクト指向について語りだすと、真の起源はどうだったとか、いや実は分派があってとか面倒だが、ここでは wikipedia に載ってるシンプルなパラダイムの分類で考えている。

┌ 命令型┬─ 手続き型
│       └─ オブジェクト指向
└ 宣言型┬─ 関数型
         └─ 論理型
一般にオブジェクト指向と関数型を対立概念と据えて語られることが多いが、こうして見てみると、COBOL や C や Java や Smalltalk みたいな命令型の一群と、Haskell や Prolog のような宣言型の一群で、大きく別れていることになる。昔から繰り返されてきたオブジェクト指向勢による手続き型叩きが(正直、自分も心当たりあるが)、一段上の観点で見直すと、命令型の枠内での内紛でしかなかったことが分かる。

2013年1月7日月曜日

煩雑な Javaコードを簡単な Clojure に直してみる

仕事始めでつかれたが、寝る前にちょっと Clojure やってみる。

以下は、Neal Ford の 『プロダクティブ・プログラマ』に載ってるのとほぼ同じコードで、ファイルを読み込んでから行番号をつけて標準出力に書き出すというもの。

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;

public class LineNumbers {
    public static void main(String...args) {
        File file = new File("sample.txt");
        LineNumberReader reader = null;
        try {
            reader = new LineNumberReader(new FileReader(file));
            while (reader.ready()) {
                System.out.printf("%d:%s%n",
                        reader.getLineNumber(), reader.readLine());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                reader.close();
            } catch (IOException ignored) {}
        }
    }
}

本ではこの後、Groovy を使うと同じ処理がいかに簡単に書けるかというくだりに続くわけだけど、今日はこれをClojure で書いてみる。

(use 'clojure.java.io)
(def zip (partial map vector))
(with-open [r (reader "sample.txt")]
  (doseq [[n line] (zip (iterate inc 1) (line-seq r))] 
    (printf "%d:%s%n" n line)))
なるほど。
  • Haskell 等でよく使う zip は、 (partial map vector) で、代用できるらしい。
  • try で囲って finally の中でリソースを開放する辺りまでを、with-open で書けるらしい。C# のあれみたいだな。
  • doseq の代わりに map とかを使うと、printf が返す nil が行数分返されてきてしまう。
  • Haskell でいう [1..] みたいのは (iterate inc 1) と掛ける。無限リストは iterate でなんとかするのが定石らしい。
  • Groovy ほどではないが、Java よりは簡潔に書ける。

寝よ。

2013年1月6日日曜日

Clojure で超簡単な問題を解いてみる

正月休みの最終日だが、また Clojure をいじってみた。

今日は、Project Euler の Problem 4をやってみる。問題の内容は、3桁の数を二つ掛け合わせて得られる最大の回文数は何かというもの。

まあ、使い慣れた言語だったら何てことない問題だけど、始めたばかりなの Clojure なので何かと難航する。 とりあえず効率は無視して書いたのがこんな感じ。

(ns euler.pe004
  (:use clojure.contrib.generic.math-functions))

(def digits 3)
(def max' (int (dec (pow 10 digits))))
(def min' (int (pow 10 (dec digits))))

(defn palindromic? [n]
  (let [original (str n)
        reversed (apply str (reverse original))]  
    (= original reversed)))

(defn solve [] 
  (apply max (for [x (range min' (inc max')) 
                   y (range x (inc max'))
                   :when (palindromic? (* x y))]
               (* x y))))

ほぼ総当たりのかなり遅いコードだけど、これだけでも、:use と :require の違いや project.clj での dependencies の指定の仕方だとか、Clojure での リスト内包表記のやり方だとかが、だんだんわかってきて面白い。

ただ、この手の問題で非効率なのはやはり気になるので、無駄な計算を排して瞬時に答えが得られるように修正してみた。

(ns euler.pe004
  (:use clojure.contrib.generic.math-functions))

(def digits 4)
(def max' (int (dec (pow 10 digits))))
(def min' (int (pow 10 (dec digits))))

(defn palindromic? [n]
  (let [original (str n)
        reversed (apply str (reverse original))]  
    (= original reversed)))
(defn cols [row] (range max' (dec row) -1))

(defn find-col [row cols max']
  (let [[col & cols'] cols 
        product (* col row)]
    (if (< product max') 0 
      (if (palindromic? product) product
        (if (seq cols') (find-col row cols' max') 0)))))
      
(defn find-product [current-max rows] 
  (let [[row & rows'] rows
        product (find-col row (cols row) current-max)
        larger (max product current-max)]
    (if (and (seq rows') (< larger (* max' (first rows')))) 
        (find-product larger rows')
        larger))) 

(defn solve [](find-product 0 (range max' min' -1)))

これだと桁数が4桁でも、元のコードで数分かかる計算が、ほぼ一瞬で終わる

しかしやっぱり、効率のために「手続き」を考慮するようになると、宣言的に書いた時の表現力が失わるっつうのは、何かと惜しいがまあしょうがないんだな。

2013年1月4日金曜日

Java の BitSet を使った Clojure の篩

昨日、Eclipse 環境で Clojure を使う準備をしたので、今日は簡単なプログラムを書いてみた。

簡単といえば Project Euler の1〜10番辺りかと見当をつけて眺めてみると、3番で素数を使っているのがあったので、エラトステネスのふるいでも書いてみることにした。

いろんな書き方があると思うけど、数ある関数型言語の中で敢えて Clojure を使うのは、やはり既存の Java 資産が使えるからというのが大きいと思うので、ここでは java.util.BitSet を使って実装してみることにした。

(ns euler.pe003)
(import 'java.util.BitSet)

(defn initial-bits [upper-bound] 
  (let [bits (BitSet. upper-bound)]
    (doall (map #(.set bits % true) (range 2 (inc upper-bound)))) bits))

(defn next-prime [pos upper-bound bits]
  (let [pos+ (inc pos)]
    (if (.get bits pos+) pos+ (next-prime pos+ upper-bound bits))))

(defn update-bits [pos upper-bound bits]
  (doall
    (map #(.set bits % false) (range (+ pos pos) (inc upper-bound) pos))) 
  [(next-prime pos upper-bound bits) bits])

(defn seive [n upper-bound bits sq] 
  (let [[m bits2] (update-bits n upper-bound bits)]
    (if (< sq m) bits2 (seive m upper-bound bits2 sq)))) 

(defn primes [upper-bound]
  (let [sq (int (Math/sqrt upper-bound))
        bits (seive 2 upper-bound (initial-bits upper-bound) sq)] 
    (filter #(.get bits %) (range upper-bound))))
 

実行してみると、以下のような結果が得られて、Haskell で計算してみたものと一致する。ただ 1,000,000 までの素数を数えているところでは、予想外に時間がかかっていた。もうちょい Clojure に慣れたら、なんで遅いのか調べてみたい。
=> (primes 100)
(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)
=> (count (primes 1000000))
78498

ここまでできたら、Problem 3 を解くのは簡単で、以下のように書き足して実行したら、正答が得られる。
(defn divmod [m n] [(long (/ m n)) (mod m n)])

(defn solve [a bits] 
  (let [[h & t] bits
        [q r] (divmod a h)]
    (if (not= r 0)
      (solve a t)
      (if (= 1 q) h (solve q bits)))))

(defn -main [& args]
  (solve 600851475143 (primes 10000)))
 
新しい言語で初めて書くプログラムとしては、当初の想定よりちょっと難しかったが、テキストを読んでるだけでは分からない事がいろいろあって、実際に書いてみるとやっぱり面白い。

2013年1月3日木曜日

Eclipse で Clojure プラグインを使ってみる

まだ入門書をちょっと読んだばかりだけど、なんか実にいい感じで期待が持てるので、Eclipse で使う準備をしてみたい。


■ インストール

"Eclipse Marketplace" で Counterclockwise で検索するとヒットするので、そのままインストール。特に記すことはない。

( Counterclockwise というのは Leiningen のプラグインで、Leiningen というのは Clojure での maven のようなもの。)


■ プロジェクト作成

File メニューとかパッケージエクスプローラ上の右クリから、[ New ] -> [ Leiningen Project ]でプロジェクト生成のダイアログボックスを開いて、適当なプロジェクト名を指定して [ Finish ]。ここでは ex01 とした。

こんな感じでファイルができてくる。


■ ソース:core.clj

プロダクトコード側の自動生成ソース。
(ns ex01.core)

(defn -main
  "I don't do a whole lot."
  [& args]
  (println "Hello, World!"))

実行は、まず右クリから [ Run As ] → [ Clojure Application ] で REPLを開く。下段に (-main) と入力すると、こんな感じで評価結果が上段に表示される。
=> (-main)
Hello, World!
nil

■ ソース:core_test.clj

テストコード側の自動生成ソース。
(ns ex01.core-test
  (:use clojure.test
        ex01.core))

(deftest a-test
  (testing "FIXME, I fail."
    (is (= 0 1))))

core.clj と同様に右クリから REPL を開いての実行だけど (run-tests)でいける。
=> (run-tests)

Testing ex01.core-test

FAIL in (a-test) (core_test.clj:7)
FIXME, I fail.
expected: (= 0 1)
  actual: (not (= 0 1))

Ran 1 tests containing 1 assertions.
1 failures, 0 errors.
{:type :summary, :pass 0, :test 1, :error 0, :fail 1}

■ 雑感

  • 上矢印キー押して、入力履歴が出てこなかったので一瞬困惑したが、Ctrl と上下矢印の同時押しでヒストリが使える。
  • コマンドラインでプロジェクトのフォルダに入れば、lein 自体も普通に使える。ただし、lein run は事前に project.clj に":main ex01.core"みたいな感じで追記する必要がある。
  • やっぱ常時 Eclipse を使ってる人には、こういうのが楽ではないかと。
  • マスコットキャラのおじさんの顔が怖い。