ClojureでStringSearchを使う

Clojureからあいまい検索が可能なJavaライブラリを使いたい

「自分で実装してもいいけど、まあ誰か作っているだろう」という思いから見つけた記事がこれ↓

高速な文字列検索を実現するJavaライブラリ「StringSearch」
http://news.mynavi.jp/column/tool/044/

どうやらやりたいことはバッチリできそう。そして、ページを進むとサンプルコードまで書いてる優しさ。

私は決めました。Clojureからこれを使おうと。

本題

project.clj を編集

ありがたいことに、Clojars に StringSearch 1.2 があるので準備は簡単。
project.cljdependenciesを追加するだけ

https://clojars.org/org.clojars.nmurray.com.eaio/stringsearch

project.clj

:dependencies [[org.clojure/clojure "1.6.0"]
               [org.clojars.nmurray.com.eaio/stringsearch "1.2"]]

追加したあとは、lein depsとかでライブラリのダウンロードを行ってください。

使ってみた

1からStringSearchのドキュメントを読むのもしんどかったので、先ほど記事のJavaコードをClojureに変換してみました。

以下のコードは参考程度に見てください。もっといい書き方がたくさんあるはず...
特にloopを含んでるものは、元のJavaコードよりも変数が増えてしまっていてかなり残念...

; http://news.mynavi.jp/column/tool/045/
; list 1

(def text (str "こんにちは世界。新世界へようこそ。"))
(def pattern (str "世界"))

(let [search (com.eaio.stringsearch.BoyerMooreHorspoolRaita.)]
  (println  (str (inc (.searchString search text pattern)) "文字目")))
;-> 6文字目
; http://news.mynavi.jp/column/tool/045/
; list 2

(def text (str "こんにちは世界。新世界へようこそ。"))
(def pattern (str "世界"))

(let [search (com.eaio.stringsearch.BoyerMooreHorspoolRaita.)
      processed (.processString search pattern)]
  (loop [x -1]
    (let [y (.searchString search text (inc x) pattern processed)]
      (if (not (= y -1))
        (do 
          (println (str (inc y) "文字目"))
          (recur y))))))
; ->
; 6文字目
; 10文字目
; nil
; http://news.mynavi.jp/column/tool/045/
; list 3

(def text (str "こんにちは世界。新世紀へようこそ。"))
(def pattern (str "世?"))

(let [search (com.eaio.stringsearch.BNDMWildcards.)
      processed (.processString search pattern)]
  (loop [x -1]
    (let [y (.searchString search text (inc x) pattern processed)]
      (if (not (= y -1))
        (do 
          (println (str (inc y) "文字目"))
          (recur y))))))
; ->
; 6文字目
; 10文字目
; nil
; http://news.mynavi.jp/column/tool/046/
; list 1

(def text "Java, java, kava, Kava, sava, ^ava")
(def pattern "[jJ]ava")

(let [search (com.eaio.stringsearch.ShiftOrClasses.)
      processed (.processString search pattern)]
  (loop [x -1]
    (let [y (.searchString search text (inc x) pattern processed)]
      (if (not (= y -1))
        (do 
          (println (str (inc y) "文字目"))
          (recur y))))))
; ->
; 1文字目
; 7文字目
; nil
; http://news.mynavi.jp/column/tool/046/
; list 3

(def pattern "Java")
(def text "Hello KaVa")
; (def text "Hello Java")
; (def text "Hello java")
; (def text "Hello JAVA")

(let [search (com.eaio.stringsearch.ShiftOrMismatches.)
      processed (.processString search pattern 2)]
  (let [x (vec (.searchString search text pattern processed 2))]
    (println (str (inc (first x)) "文字目"))
    (println (str "    " (second x) "文字のエラー"))))

おまけ1: import でオブジェクト生成を短く書く

(let [search (com.eaio.stringsearch.BoyerMooreHorspool.)]
  (println  (str (inc (.searchString search text pattern)) "文字目")))

com.eaio.stringsearch.BoyerMooreHorspool.って長いですよね...

Java同様、importで短くしましょう。

(import '(com.eaio.stringsearch))

(let [search (BoyerMooreHorspool.)]
  (println  (str (inc (.searchString search text pattern)) "文字目")))

または

(ns yournamespace
  (:import (com.eaio.stringsearch)))

(let [search (BoyerMooreHorspool.)]
  (println  (str (inc (.searchString search text pattern)) "文字目")))

おまけ2: 公式のドキュメントはどこに?

こちらが公式
http://johannburkard.de/software/stringsearch/

しかし、Doocumentation のページを辿っても Usage 的なものはないので…

StringSearch 1.2 tar.gz をダウンロード・展開した先にあるindex.htmlを読むのがおすすめ。(Javaだけど)

stringsearch-1.2/documentation/index.html

おまけ3: 日本語のあいまい検索がうまくいかない!

残念ながら、できません。

ShiftOrMismatches だけ日本語のようなマルチバイトに対応してません...

詳細は "おまけ2" でダウンロードしたファイルの中に書いてあります。

stringsearch-1.2/documentation/usage.html

感想

  • JavaのライブラリをClojureから呼び出す勉強になった!

  • ClojureJavaと比べてオブジェクト生成時のコードが短くて素敵!(ほぼ誤差)

    Java
    StringSearch search = new BoyerMooreHorspoolRaita();

    Clojure
    let [search (BoyerMooreHorspool.)]

  • Javaのライブラリを呼ぶと全然短くならない!(たぶん事実)

  • fireplace.vim で補完が効かないので、APIドキュメントを参照しないとtypoする!

3分でJavaの環境構築から実行までを思い出すため

Javaを忘れた

Androidアプリ開発はjavaで書いていたとはいえ、それはもう1年以上前の話。

しかも、開発環境は Eclipse

絶対に必要な class などは最初から書いてくれてたし、なにが最低限必要なのかなんて覚えてなかった(言い訳)

なので、LinuxJava を書く流れを3つに分けて書く

環境構築

メインPCのOSがWindowsからDebianに変わってしまってからは
EclipseなどのIDEは触らなくなった(ほとんどvimでやりたいことができるから)

つまりJavaコンパイラのインストールとかも初めて(笑)

とりあえず今回はOracleJavaは使わずに、aptで簡単に入れれるOpenJDKを使用する。

OpenJDK(Javaコンパイラとかもろもろ)をインストール

sudo apt-get install openjdk-7-jdk

以下のコマンドを叩いて

javac

こんなのがわらわら出てこればOK

使用方法: javac <options> <source files>
使用可能なオプションには次のものがあります。
  -g                         すべてのデバッグ情報を生成する
  -g:none                    デバッグ情報を生成しない
  -g:{lines,vars,source}     いくつかのデバッグ情報のみを生成する
  -nowarn                    警告を発生させない

  :

Hello world を書く

どこのディレクトリにコードを置くかは自由ですが、
Hello worldは一度実行したっきりいらないので、今回は/tmpに置きます。

cd /tmp
touch HelloWorld.java
vim HelloWorld.java

HelloWorld.javaに以下をコピペ

public class  HelloWorld  {
    public static void main(String[] args) {

        // 普通に Hello world を表示
        System.out.println("Hello world.");

        // オブジェクトを作ってメンバ関数を実行
        HelloWorld obj = new HelloWorld();
        if (args.length != 0) {
            for (int i=0; i<args.length; i++) {
                System.out.println(obj.sayHello(args[i]));
            }
        }
    }

    public HelloWorld() {
    }

    public String sayHello(String arg) {
        return "  Hello " + arg + " !";
    }
}

コンパイル・実行

コンパイル*.javaファイルを引数にjavacコンパイル

javac HelloWorld.java

*.classファイル出来上がるはず

HelloWorld.class

クラス名を引数に実行

java HelloWorld

実行結果

Hello world.

さっき書いたコードはコマンドライン引数を取るようにしておきました

java HelloWorld hoge foo huga

実行結果

Hello world.
  Hello hoge !
  Hello foo !
  Hello huga !

おわり

3分で思い出せたかな??

GStreamer についてちょっとだけ

GStreamer ???

GStreamer は、凝ったGUIを持ったものから、特定の形式の動画の再生をするだけといったシンプルなのものまで、様々なプレイヤーを作成できる。
マルチメディアのマルチプラットフォーム対応ライブラリです。
GStreamer: features

Windows, OS X, Linux 対応
 iOS, Android でも使えます。

公式のドキュメント(サンプルやHowto)が充実しているので、
Google先生に頼らずに、勉強しやすいのも特徴(?)

バージョンについて

GStreamerには、0.10 と 1.0 の2つのバージョンがあり、
ネットでサンプルコードを探して出てくるものの多くが 0.10 です。(個人的な感想)

公式の Tutorial も 0.10 で書かれています。。。

また、debian wheezy (現在のstable) に入っている GStremaer は 0.10 のみですが、
次期 stable の jessie からは 1.0 も入れることが可能です。

どっちがいい?

ゴリゴリに使っている人じゃないので、正直わかりません。

ですが、1.0 をはじめとする、1.x 系は、公式でも "targeted at end users." と書かれてますし、 iOSAndroid などで GStreamer を使いたい場合は 1.0 がいいかもしれません。

バグ修正や新規開発が進んでいるのも 1.x 系ですし…

違いは?

メジャーバージョンが変わっていることからもわかりますが、 0.10 と 1.0 の間にはAPIの互換性がありません。

マクロや関数に手がたくさん入っています。

開発環境内に 0.10 と 1.0 のライブラリを共存させることは可能ですが、
1つのプログラム内で併用するとかそういうヤバイことはやめたほうがいいと思います。(やったことない)

0.10 のサンプルしか見当たらない!

そんな時は 1.0 へ Porting するのが1つの手です。

Porting作業時に頼りになる公式ドキュメントがこちら
GStreamer 0.10 to 1.0 porting guide

ちなみに、公式の Tutorialのサンプルコードはすでに Porting されています。
http://cgit.freedesktop.org/~slomo/gst-sdk-tutorials/

Tutorial 内のコードと見比べながらサンプルコードを動かすと勉強になりそうですね。

vim から GNU Screen を叩く

f:id:shocrunch:20150222015333g:plain

vim から GNU Screen を叩く

GNU Screen の -X オプションをラップした vim-slime が便利です。

vim-slime: https://github.com/jpalardy/vim-slime

オリジナルは jpalardy さんのものですが、いくつか fork されているので
そちらも目を通してみると、有用な情報が手に入るかもしれません。

インストール

Neobundle でインストールが可能です。

NeoBundle 'jpalardy/vim-slime'

使い方

使い方は :help slime.txtを参照すればわかりますが、普通に使うものだけ紹介

  • normal mode で<C-c><C-c>で、 vim の buffer を送信
  • visual mode で<C-c><C-c>で、選択中の文字を送信
  • normal mode で<C-c>vで、session name, window name を再設定する

また、初回実行時には screen の session name と window name の入力を求められます。
これを適切に設定してあげる必要があります。

  • session name って?? なにを入力すればいいの??
    • 端末上で screen -ls と打ち込むと以下のように出てくる。
      4909.pts-0.bigcool などが session name です。
        There are screens on:
            7984.pts-6.bigcool    (2015年02月22日 00時05分08秒)   (Detached)
            7956.pts-4.bigcool    (2015年02月22日 00時05分05秒)   (Detached)
            4909.pts-0.bigcool    (2015年02月21日 23時20分55秒)   (Attached)
        
    • この表示結果をコピペしてもいいですが<tab>補完が効くので安心してください。
    • いま開いている screen の sessionname を知りたいときは、
      screen のエスケープシーケンスを叩いて:sessionnameと打てば教えてくれます。

自分の都合のいいようにパッチを書いた

vim-slime は使いやすいんですが、
複数の window に対してコマンドを一斉送信したかったのでちょっといじりました。

パッチの効果

  • 複数の window に対して送信できるようにする
    • 想像していたような動きをしなかった場合はman screen内の、atを参照してください。 解決策がみつかるかもしれません。
  • screen 内で vim を開いている場合、session name を自動的に設定する

パッチの当て方

Neobundle でインストールした人を対象に説明します。

  1. パッチをダウンロードする
    下に貼ってあるパッチをすべてコピーし、適当なファイル名で保存してください。(例えば patch.txt とか)

  2. パッチを当てる準備
    "1."でダウンロードしたパッチを、$HOME/.vim/以下にあるvim-slimeのディレクトリへコピーします
    sh $ cp patch.txt ~/.vim/bundle/vim-slime/

  3. パッチを当てる
    パッチのコピー先へ移動して、パッチを当てます。 sh $ cd ~/.vim/bundle/vim-slime/ $ patch -p1 < patch.txt

作成したパッチ

diff --git a/plugin/slime.vim b/plugin/slime.vim
index f94b36a..79267f2 100644
--- a/plugin/slime.vim
+++ b/plugin/slime.vim
@@ -19,7 +19,7 @@ end
 function! s:ScreenSend(config, text)
   call s:WritePasteFile(a:text)
   call system("screen -S " . shellescape(a:config["sessionname"]) . " -p " . shellescape(a:config["windowname"]) . " -X readreg p " . g:slime_paste_file)
-  call system("screen -S " . shellescape(a:config["sessionname"]) . " -p " . shellescape(a:config["windowname"]) . " -X paste p")
+  call system("screen -S " . shellescape(a:config["sessionname"]) . " -p " . shellescape(a:config["windowname"]) . " -X at " . shellescape(a:config["windowname"]) . "# paste p")
 endfunction
 
 function! s:ScreenSessionNames(A,L,P)
@@ -28,7 +28,7 @@ endfunction
 
 function! s:ScreenConfig() abort
   if !exists("b:slime_config")
-    let b:slime_config = {"sessionname": "", "windowname": "0"}
+    let b:slime_config = {"sessionname": $STY, "windowname": "0"}
   end
 
   " screen needs a file, so set a default if not configured

suffix aliasでファイル1つのコードをコンパイル

深夜に「ファイル1つのコードをコンパイルするのがめんどくさい
という話を書きました。

これをtwitterに展開したところ、「僕はsuffix aliasでやってる」 という情報を頂きました。ありがとうございますm(__)m

調べてみると、想像以上に便利。

suffix aliasを使うとコンパイルから実行までこうなる

$ ls
hello.c
$ ./hello.c
Hello world.
$ ls
hello hello.c

./hello.cと打ち込むだけでコンパイル実行を一気やってくれるようにできます。 これはいい :-)

設定は.zshenv.zshrcfunctionalias -sを追加するだけ

function run_c()
{
    EXECUTABLE=`echo $1|sed -e s/.c$//`
    gcc -Wall -Wextra $1 -o ${EXECUTABLE}
    shift
    ${EXECUTABLE} $@
}
alias -s c=run_c

実行ファイルなんていらないんじゃ
という方は/tmpに一定の名前で吐き出してあとから消すとか...?

function run_c()
{
    EXECUTABLE=/tmp/a.out
    gcc -Wall -Wextra $1 -o ${EXECUTABLE}
    shift
    ${EXECUTABLE} $@
    rm -f ${EXECUTABLE}
}
alias -s c=run_c

suffix aliasで検索するといろんなサイトが出てくるので、詳細はそちらで。

2/14 14:04 追記

run_c()のsedを修正

たった1つのソースコードをコンパイルするのがめんどくさい

目的・目標

ライブラリなどの勉強を始めた頃は、1つのCソースファイルで完結するものが多い。
そんなちっさなプログラムのコンパイルをもっともっと楽にしたい

起きている問題

例えば Hello world を作るとこから実行まではだいたいこんな感じ

$ vim hello.c
$ gcc -Wall hello.c -o hello
$ ./hello

いちいち、gcc -Wall hello.c -o helloなんて打ちたいくない
なので、makefileを作ると...

$ vim hello.c
$ vim Makefile
$ make
$ ls
hello hello.c Makefile
$ ./hello

コンパイルは簡単になった。
しかし、新たにプログラムを書くと

$ ls
hello hello.c Makefile
$ vim hoge.c
$ vim Makefile
$ make hoge
$ ls
hello hello.c hoge hoge.c Makefile
$ ./hoge

コンパイルは変わらず簡単だ。
しかし、Makefileの編集から逃れられず、めんどくさい。

しかも、プログラムが増えるとMakefileが以下のようになってくる...

hello:
    $(CC) $(CFLAGS) hello.c -o $@
hoge:
    $(CC) $(CFLAGS) hoge.c -o $@
foo:
    $(CC) $(CFLAGS) foo.c -o $@

どんどん増える。
でも、違うところは"ソースファイル名"だけ

ダサい :-(

とりあえず打ったダサい対応策

[Shell Script] =Call=> [Makefile]

こんな感じに、Shell Script から make すれば Makefile の編集作業を消せるね。
打ち込むコマンドは ./make_script hello とか ./make_script hoge とか

これなら許せる。

Makefile

CFLAGS=-Wall -g

SRC=$(EXEC:=.c)

$(EXEC):
    $(CC) $(CFLAGS) $(SRC) -o $@

make_script

#/bin/bash

make EXEC=$1

思ってること

Makefileだけで同様のことができる気がしてならない...
なんかうまいことできる方法を知っている方教えてください...

shake 0.4.0 と clojure 1.6.0 はNG

shake を試そうとしたけど repl が立ち上がらない

こんな感じのメッセージが出て立ち上がらない...

Exception in thread "main" java.lang.IncompatibleClassChangeError: Found class clojure.asm.ClassVisitor, but interface was expected, compiling:(/tmp/form-init786032944266256305.clj:1)
    at clojure.lang.Compiler.analyzeSeq(Compiler.java:6463)
    at clojure.lang.Compiler.analyze(Compiler.java:6260)
    at clojure.lang.Compiler.eval(Compiler.java:6509)
    at clojure.lang.Compiler.load(Compiler.java:6970)
    at clojure.lang.Compiler.loadFile(Compiler.java:6930)
    at clojure.main$load_script.invoke(main.clj:274)
    at clojure.main$init_opt.invoke(main.clj:279)
    at clojure.main$initialize.invoke(main.clj:307)
    at java.lang.Thread.run(Thread.java:745)
:

原因は clojure のバージョンに shake が追いついていないこと
ま、shake の開発止まってるししょうがないよね ;)

ダメな組み合わせ

; project.clj
  :dependencies [
                 [org.clojure/clojure "1.6.0"]
                 [shake "0.4.0"]
                 ])

大丈夫な組み合わせ

; project.clj
  :dependencies [
                 [org.clojure/clojure "1.5.0"]
                 [shake "0.4.0"]
                 ])

clojure 1.5.0 は行ける。 java io じゃなくて、shake 使いたいんじゃ!! って場合は、ちょっとバージョンを落とす必要があるみたい