読者です 読者をやめる 読者になる 読者になる

CodeIQのRestricted Words問題をClojureで

CodeIQで次のような問題(通称「Restricted Words」問題)が出されていました。

標準出力に
Hello World
と出力するプログラムを作成して下さい。

ただし、数値、文字及び文字列リテラルを解答に含めることはできません。
Perlのqqやqw、Rubyの%Q、%q、%wなども避けたほうが評価が高くなります。
言語仕様をフル活用して下さい!

ログイン│CodeIQ

9/19に解答期限が切れて,自分の解答を公開することができるようになったので,
Clojureで書いて提出した解答について適当に紹介してみようと思います。

Clojureで解答を考えるにあたって,以下の方針を立てました:

  • シンボルやキーワード,正規表現などのリテラルも使用しない
    • 文字列を使えるのとほとんど変わらず,チートっぽい
  • マクロを使用しない
    • シンボルが使いたい放題になるのでマクロも使わない
  • 文字コードを算出して一文字ずつ表示」する方法は使わない
    • 面倒なうえにおもしろくない


ここまで制約を課すと,取れる方法はある程度限られてきます。
僕が考えたのは,「コード中に書いた"Hello"や"World"といった
識別子の情報をどうにかしてとってくる」方法です。その「どうにかして
とってくる」方法を2つほど考えました。

解答その1

(defn print-names [& vals]
  (apply println
         (for [val vals, [_ var] (ns-publics *ns*)
               :when (= val @var)]
           (.sym var))))
 
(declare Hello World)
(print-names Hello World)


まず考えたのはあらかじめ定義しておいたVarから名前をとってくる方法。Varの名前は(.sym var)で取得できます。もしくは,Varのメタデータを使って (:name (meta var)) でも取得できます。
Var自体を取得する方法としては,ここではVarをあらかじめユニークな値に束縛しておいて,名前空間からそのユニークな値に束縛されたVarを見つけてくる方法を取っています。もちろん,素直に #'Hello でとってこれはするんですが,このフォームもリテラルっぽい感じで今回は使用を控えました。

と,ここまでが提出した解答その1なんですが,Varの名前を使う方法は突き詰めると下のコードまで短くできることに〆切が過ぎてから気づきました。

(println (.sym (def Hello)) (.sym (def World)))

解答その2

(defn get-error-message [thunk]
  (try
    (thunk)
    (catch Exception e
      (.getMessage e))))
 
(defn transpose [xs]
  (apply map list xs))
 
(defn drop-common-prefix [& strs]
  (->> (transpose strs)
       (drop-while #(apply = %))
       (transpose)
       (map #(apply str %))))
 
(declare Hello World)
(let [hello-msg (get-error-message #(Hello))
      world-msg (get-error-message #(World))]
  (apply println (drop-common-prefix hello-msg world-msg)))


次の解答はエラーメッセージから文字列を抽出する方法をとりました。Clojureが発生させる例外の中には,メッセージに変数名を含むものがあるので,その例外をキャッチしてメッセージから変数名を抽出します。
発生させる例外はなんでもそれほど違いはありませんが,コンパイラが投げる類の例外Clojure例外処理ではキャッチできないのでそういうものは避ける必要があります。たとえば,以下の例外はキャッチできません。

user=> hoge ; 未定義の変数hogeの参照はCompilerExceptionを発生させる
CompilerException java.lang.RuntimeException: Unable to resolve symbol: hoge in this context, compiling:(NO_SOURCE_PATH:0:0)
user=>
user=> (try hoge (catch RuntimeException e (.getMessage e))) ; try-catchではキャッチできない
CompilerException java.lang.RuntimeException: Unable to resolve symbol: hoge in this context, compiling:(NO_SOURCE_PATH:19:1) 
user=> 

今回は,未束縛のVarを関数適用した際に出る例外を採用しました。

user=> (declare Hello)
#'user/Hello
user=> (Hello)
IllegalStateException Attempting to call unbound fn: #'user/Hello  clojure.lang.Var$Unbound.throwArity (Var.java:43)
user=> (try (Hello) (catch IllegalStateException e (.getMessage e)))
"Attempting to call unbound fn: #'user/Hello"
user=> 

残すはこのエラーメッセージの末尾のHelloを切り出してくるだけですが,問題は数値や文字,文字列,正規表現を使えない状況でどうやって所望の文字列を切り出してくるか。ムリクリ数値を作り出してsubstringで切り出すことはできなくはないですが,ダルいのでできればやりたくない。今回の解答では発想を転換して,Hello用のエラーメッセージとWorld用のエラーメッセージを用意して,共通部分を取り除く方法を使いました。

おわりに

今回のお題は「言語仕様をフル活用して下さい!」とのことだったので,Clojureのいろんな機能を使って解答を考えてみました。おかげで,細々とした部分の挙動(特にUnboundオブジェクトの扱いなど)で新しい発見もあって理解が深まりました。
まぁただし,ここに掲載したコードは,Clojureのバージョンが上がったら結果が変わる可能性があるような微妙な振る舞いに依存している部分もあるので,なにかの参考にする場合には注意が必要かもしれません。

おまけ

キーワードとマクロを使った解答も考えてみました。

(def alphabets
  (for [i (range)
        :let [c (char i)]
        :while (not= c (first (name :|)))
        :when (Character/isLetter c)]
    c))

(defmacro with-alphabets [& body]
  `(let [~(mapv #(symbol (str %)) alphabets) alphabets]
     ~@body))

(with-alphabets
  (println (str H e l l o) (str W o r l d)))