Programmer's Note

コード読み書きの備忘録。

ClojureとSwingでボール落ちるアニメーション

ClojureからSwingを使って簡単な描画アニメーションを試してみるテスト。 ClojureJavaの世界をかなり透過的にたたけるが、書き方のパターンさえ覚えてしまえば、コード量が全然少なくなるので、はっきり言ってJavaで書くよりラクだ(笑)。

とりあえず、上から下に小さなボールが落ちていくアニメーション作ってみた。

f:id:hifistar:20160413235942p:plain

ソースコード

(ns test-paint.core
  (:import [javax.swing JFrame JPanel Timer]
           [java.awt.event ActionListener])
  (:gen-class))

(def py (atom 0))

(defn next-y
  [y h]
  (if (<= y (- h 10)) (inc y) y))

(def panel
  (proxy [JPanel ActionListener] []
    (paintComponent [g]
      (let [w (proxy-super getWidth)
            h (proxy-super getHeight)
            x (/ w 2)
            y (next-y @py h)]
        (doto g
          (.setColor java.awt.Color/BLACK)
          (.fillOval x y 10 10))
        (reset! py y)))
    (actionPerformed [e]
      (.repaint this))))

(defn -main
  [& args]
  (let [frame (JFrame. "Test Paint")
        timer (Timer. 10 panel)]
    (doto frame
      (.add panel)
      (.pack)
      (.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE)
      (.setSize 400 400)
      (.setVisible true))
    (.start timer)))

Clojureでjavax.swing.JFrameを使ってみる

Clojureからjavax.swingを使い、GUIプログラミングをかじり始めつつ、Javaメソッド呼び出しのやり方も練習もする。

Javaメソッドの呼び出し方

例えば、文字列Helloの長さを返す。
repl=> (.length "Hello")
5

これはJava"Hello".length()を呼びだしたと同じ。

staticメソッドを呼び出す
repl=> (java.lang.Math/abs -1)
1

これはそのままで分かりやすい。 デフォルトの状態でjava.langはnamespaceにはimport済みのようなので、下記も同様の動きをする。

repl=> (Math/abs -1)
1

JFrameを使ってウィンドウを出す

いつものとおり、leinで新しいプロジェクトを作って試す。

swing $lein new app hello_frame

単純に400x400の大きさのフレームを出してみる。

以下ソース。

(ns hello-frame.core
  (import (javax.swing JFrame))
  (:gen-class))

(def frame (new JFrame))

(defn -main
  [& args]
  (.setSize frame 400 400)
  (.setTitle frame "Hello,JFrame")
  (.setVisible frame true))

JFrameのインスタンス(new Frame)で生成し、以降(.setSize ...)などでメソッド呼び出しをしている。

(.setSize frame 400 400)とかは、実はマクロを使っていて、内部的にはドットスペシャルフォームに変換される。(.を使った特殊形式)

macroexpand-1を使って展開してみると、以下のようになる。

repl=> (macroexpand-1 '(.setSize frame 400 400))
(. frame setSize 400 400)

逆に言うと、.特殊フォームを使っても書ける訳で、下記も同じ動作をする。

(ns hello-frame.core
  (import (javax.swing JFrame))
  (:gen-class))

(def frame (new JFrame))

(defn -main
  [& args]
  (. frame setSize 400 400)
  (. frame setTitle "Hello,JFrame")
  (. frame setVisible true))

こっちの方が分かりやすいよな。と個人的に思ったが・・・。

さて、Javaのクラスのインスタンスを作るのは (new JFrame)でできるが、こちらは(JFrame.)という表記でも可。

引数も与えることができて、 (JFrame. "Heeloooo")とするとタイトルの文字列を与えてインスタンス生成する。

同じオブジェクトのメソッドを連続して呼び出したい時に、便利なマクロdotoが用意されている。

今回の場合では、frameに対して、setSizesetTitlesetVisibleを呼びたい。以下のように書ける。

  (doto frame
    (.setSize 400 400)
    (.setTitle "Hello,JFrame")
    (.setVisible true)))

うむ、便利。

とりあえず、JFrameをnewするときにタイトルも渡すようにすると、最終ソースは下記のようになる。

(ns hello-frame.core3
  (import (javax.swing JFrame))
  (:gen-class))

(def frame (JFrame. "Heeellooo!!"))

(defn -main
  [& args]
  (doto frame
    (.setSize 400 400)
    (.setVisible true)))

参考文献

Working with the JVM | Clojure for the Brave and True

fireplace使用メモ

vimClojureの開発が便利になるプラグイン vim-fireplace を導入してみた。

インストール

参照したサイト: http://blog.ieknir.com/blog/beginning-clojure-with-vim/

自分はNeoBundleを使っているので、以下の2つのプラグインを.vimrcに追加しただけ。

 call neobundle#begin(expand('~/.vim/bundle/'))
...
NeoBundle 'tpope/vim-fireplace'
NeoBundle 'tpope/vim-classpath'
...
call neobundle#end()

fireplaceは裏でREPLと通信して、vimでエディットしながら、その場で式の評価などができるので、かなり便利だ。 とりあえずメモ。

vimでコマンド実行するまでの手順

先にREPL環境を立ち上げないとvimでのコマンド実行できない。

(1) leiningen を使ってプロジェクトを作成

$ lein new test

(2) プロジェクト直下に入りlein replを実行する

$ cd test $ lein repl

(3) vimを立ち上げプロジェクト内のファイルを編集する

$ vim src/test/core.clj

関数の評価

vimコマンドラインでREPLの機能を呼び出せる。これだけでかなり便利だ。

下記例は標準関数を使用しているが、自分が作成した関数でも可能だ。 その場合、一旦:wなどでファイル保存してから、関数を使用する必要がある。

vimのコマンド入力で:Evalを使う。

:Eval (println "hello")

結果:

hello
nil

関数のdocを見る

:Doc println

結果:

clojure.core/println
([& more])
  Same as print followed by (newline)

ソースを見る

:Source println

結果:

(defn println
  "Same as print followed by (newline)"
  {:added "1.0"
   :static true}
  [& more]
    (binding [*print-readably* nil]
      (apply prn more)))

キーバインド

cpp

カーソルの行を評価する

例:

(println "hello")

のときに、括弧の中のどこにカーソルを置いてもよく、cppと打てば、helloと実行結果が最下行のバッファに表示される。

cppは複数行にまたがる関数の実行にも使える。→cp)の項目を参照。

cp)

カーソルの下の(と対になる)までの関数を実行する。 例えば

  (loop [x 1]
     (if (> x 11)
       [x]
       (recur (+ x 1))))

があったときに、(loop ...のはじめの(にカーソルを合わせて、cp)と打つと、結果として[12]が最下行のバッファに表示される。

上記は(loop ...)の最初の(と最後の)にカーソルを合わせて、cppと打っても、同じ結果になる。(こちらの方が簡単か)

cpr

カレントnamespaceに対して(require :reload)を実行する

例:

(defn boo []
   (take 6 (iterate inc 0)))
(boo)

としたエディットした直後だと、(boo)にカーソルを置いてcppを打っても、シンボルbooが見つからないと怒られてしまう。 この場合に、 cprを打ってからcppと打つとうまく行く。

K

カーソルの下の関数の(doc ..)の結果を表示する

[d

カーソルの下の関数のソースを表示する

[C-d

カーソルの下の関数のソースに飛ぶ

cqp

REPLのプロンプトが出てくる。

Clojureのプログラミング本とか

先週あたり、ひととおり「プログラミングClojure」を読み終えた。

この本は正直、分かりやすくない。 用語が統一されてない部分があったり、用語の説明がいまいち足りなく、 ネットで検索せんといかんかったり。

サンプルコードも面白い訳でもなく、かといってリファレンスとしても使えないし、 とまあ結構さんざんな言いようだが・・・原著はRich Hickeyさんが ブックリスト に追加しているくらいだから、結構評価されている本なんでしょうね。 (自分には合わんかったというだけかな)

さて、もうちょっとLispのプログラミングを知ろうと思って、 SICP を読み始めた。英語のペーパーバック。 (以前、しょっぱなの数ページだけ読んで止めていた)

一方で、たまたまオンラインでフリーで読める本を見つけた。 Learn to Program the World's Most Bodacious Language with Clojure for the Brave and Trueは、なかなか面白そうだ。

おちゃらけた感じで書かれてるが、こーいうのは好きだな。 (Joel on Softwareもそんな感じだしな。) でもしょっぱなからClojureのクールなところを感じさせる部分を紹介していたり、 なかなかセンスよさげ。これもあとで紙本を買って読みたいところだ。

Clojureメモ: 再帰を使うとか使わないとか

再帰呼び出し。 練習がてら下記のような関数を作ってみる。

(myfunc 5 [])
=> [1 2 3 4 5]

考え方としては、

(myfunc 5, [])
  -> (myfunc 4, [5])
     -> (myfunc 3, [4 5])
        -> (myfunc 2, [3 4 5])
           ...
           -> (myfunc 0, [1 2 3 4 5])
                => 最後に[1 2 3 4 5]を返す

実装すると以下のようになる。

(defn myfunc [n coll]
  (if (<= n 0)
    coll
    (myfunc (dec n) (cons n coll))))
t-recur-func.core=> (myfunc 5 [])
(1 2 3 4 5)

これは、引数にコレクション(ベクター)を渡してしまうが、 コレクションを渡さない以下のやり方も考えられる。

(defn myfunc2 [n]
  (if (<= n 1)
    [n]
    (conj (myfunc2 (dec n)) n)))
t-recur-func.core=> (myfunc2 5)
[1 2 3 4 5]

こっちは、呼び出した関数の返り値を、どんどんくっつけていくパターン。

どちらかというと、個人的にはこちらの方が実装のイメージがしやすかった。

ただし、こちらの方が末尾再帰ではないので、(recur ..)に置き換えることができず最適化できない。 (再帰呼び出しの回数が多いとスタックオーバーフローを起こす)

とはいえ、このパターンでフィボナッチ数列を求めることもできる。

(defn fib [a b n]
    (if (<= n 1)
      [a] 
      (cons a (fib b (+ a b) (dec n)))))
t-recur-func.core=> (fib 0 1 7)
(0 1 1 2 3 5 8)

てな感じだが、 最後の一行がごちゃごちゃしてて、あまり分かりやすくない…。

フィボナッチ数列の求め方は、Clojure的にはやはり、再帰を使わないやり方がエレガント。 以下のような感じ。

(defn fib2 [n]
  (letfn [(_fb [[a b]]
            [b (+ a b)])]
    (take n
        (map first (iterate _fb [0 1])))))
t-recur-func.core=> (fib2 8)
(0 1 1 2 3 5 8 13)

これは「プログラミングClojure」に載っていた解法。 (ポイントを思い出して書いたので、そのままではない)

一旦、

([0 1] [1 1] [1 2] [2 3] [3 5]...)

のシーケンスを求めといて、 それぞれの最初の要素を取り出してしまう。という考え方。

まあ、mapとかiterateとかの仕様を知らないと読めないのだが。 でも、Clojureレバレッジを最大に生かすこの解法には膝を打ったな。

Clojureメモ: マクロの動き

マクロの使い方覚え書き。

まず、「プログラミングClojure」に出ていたノーマルな定義。

unlessというマクロ。

(defmacro unless [expr form]
    (list 'if expr nil form))

実行結果

t-macro.core=> (unless false (println "1"))
1
nil
t-macro.core=> (unless true (println "1"))
nil

Clojureのマクロは、Cのプリプロセッサと同様に、 定義された内容はいったん展開され、その後式として評価される。

(macroexpand-1)を使うと展開した後の式が見える。

t-macro.core=> (macroexpand-1 '(unless true (println "1")))
(if true nil (println "1"))

マクロの中身は(list 'if expr nil form)であるが、 このうちexprformは引数として渡され、評価されずにそのまま展開される。

一方、(list ...)は評価されて、これがマクロの結果として返される。 'ifはリーダマクロ'を使ってifを評価せずにそのまま渡している。 nilは評価されるがnilになる。

ここがポイントのようで、Cのプリプロの単純な文字列置換とはちょっと違う。

マクロを以下のように書き換えてみる。unless-2とする。

(defmacro unless-2 [expr form]
    (if expr nil form))

さっきとの違いは、(list...)を外し、'ifではなくifを使った。

実行してみる。

t-macro.core=> (unless-2 false (println "1"))
1
nil
t-macro.core=> (unless-2 true (println "1"))
nil

一見、結果は一緒。

しかし、(macroexpand-1)すると違いが分かる。

t-macro.core=> (macroexpand '(unless-2 false (print "1")))
(print "1")

マクロとして展開された結果が違う。 今度のはifがマクロ展開時に実行されたので、その結果がマクロ展開の結果として返っている。

上記を見ると、マクロ定義は簡単に言えば 何かしらのClojureプログラムのリストを返す関数を作ること、と思えばよさそうだ。

とすると、さきほど一見、unlessunless-2の実行結果が同じに見えたが、 以下を試してみると、、、

t-macro.core=> (unless (= 1 2) (println "1"))
1
nil

t-macro.core=> (unless-2 (= 1 2) (println "1"))
nil

結果は違ってくる。 macroexpand-1してみる。

t-macro.core=> (macroexpand-1 '(unless (= 1 2) (println "1")))
(if (= 1 2) nil (println "1"))

t-macro.core=> (macroexpand-1 '(unless-2 (= 1 2) (println "1")))
nil

unless-2はなぜnilを返すのだろう?

unless-2はマクロとしては、(if '(= 1 2) nil '(println "1"))を実行する形になる。

t-macro.core=> (if '(= 1 1) "yes" "no")
"yes"

t-macro.core=> (if '(= 1 2) "yes" "no")
"yes"

(if..)関数は、評価なしのformをそのまま渡すと true として見なすようだ。

なお、

t-macro.core=> 'true
true
t-macro.core=> 'fase
fase

で、true, falseをquoteしてもtrue, falseになるようだ。 これ忘れるとハマるな。

vimrcメモ(clojure用plugin設定)

vimrcメモ。 Clojureのための設定も追加。

set nocompatible

set runtimepath^=~/.vim/bundle/neobundle.vim/
call neobundle#begin(expand('~/.vim/bundle/'))

NeoBundleFetch 'Shougo/neobundle.vim'
NeoBundle 'guns/vim-clojure-static'
NeoBundle 'kien/rainbow_parentheses.vim'

call neobundle#end()

" Enable file type detection and do language-dependent indenting.
filetype plugin indent on

NeoBundleCheck

" Switch syntax highlighting on
syntax on

" Show line numbers
set number

" C indent setting
set cindent
set cinoptions+=:0 "Change indent rule of 'case' statement

" Tab width, no expand to space
"set tabstop=4 shiftwidth=4 noexpandtab
set tabstop=4 shiftwidth=4 expandtab

" Highlight search word, increamental hightlight
set hlsearch
set incsearch

autocmd QuickFixCmdPost [^l]* nested cwindow
autocmd QuickFixCmdPost     l* nested lwindow

" show buffer directly
map <C-b> :ls<CR>:buf

" rainbow_parentheses.vimの括弧の色付けを有効化
au VimEnter * RainbowParenthesesToggle
au Syntax * RainbowParenthesesLoadRound
au Syntax * RainbowParenthesesLoadSquare