2012年12月30日日曜日

Clojureでテキスト入出力

Clojureでテキスト入出力を行う方法のメモ。

1 テキストファイル

;; 出力
(spit "test.txt" "Hello, World")

;; 入力
(assert (= "Hello, World" (slurp "test.txt")))

2 文字列

(with-open [in (java.io.StringReader. "Hello")]
  (assert (= \H (char (.read in))))
  'ok)

(with-open [in (clojure.java.io/reader (.getBytes "Hello"))]
  (assert (= "Hello" (.readLine in)))
  'ok)

(with-in-str "hoge"
  (assert (= "hoge" (read-line))))

3 XML

;; [org.clojure/data.xml "0.0.6"]
(require '[clojure.data.xml :as xml])
(xml/parse-str "<x id='1'><y>a</y><y id='2'>b</y></x>")

;; [enlive "1.0.0"]
(require '[net.cgrand.enlive-html :as enlive])
(let [s "<x id='1'><y>a</y><y id='2'>b</y></x>"
      x (with-in-str s (enlive/xml-resource *in*))]
  (assert (= '("b") (:content (first (enlive/select x [:#2]))))))

4 CSV

;; [org.clojure/data.csv "0.1.2"]
(require '[clojure.data.csv :as csv])

(let [s "a,\"b,c\",d\ne,fg,h\n"]
  (println (csv/read-csv s))
  (csv/write-csv *out* (csv/read-csv s))
  (assert (= s (with-out-str
                 (csv/write-csv *out* (csv/read-csv s)))))
  'ok)

5 JSON

;; [org.clojure/data.json "0.2.0"]
(require '[clojure.data.json :as json])
(let [m (json/read-str "{\"k1\": 1, \"k2\": \"val\"}")]
  (println m)
  (assert (= m (json/read-str (json/write-str m))))
  (with-open [out (clojure.java.io/writer "tmp.json")]
    (json/write m out))
  (with-open [in (clojure.java.io/reader "tmp.json")]
    (assert (= m (json/read in))))
  'ok)

;; オプションとして :key-fn や :value-fn を指定すると read/write 時にキーと値を変換できる
(println (= {:key 1.0}
            (json/read-str "{\"key\" : 1}"
                           :key-fn keyword
                           :value-fn #(double %2))))

6 INIファイル

;; [clojure-ini "0.0.1"]
(require '[clojure-ini.core :as ini])

(spit "test.ini" "[a]\nb=c\nd=e\n")
(assert (= {:a {:b "c", :d "e"}}
           (ini/read-ini "test.ini" :keywordize? true)))

2012年12月16日日曜日

[メモ]衛星の高度から速度・周期を求める

衛星の高度から速度・周期を求める式が JAXAのページ に書いてありました。

6378[km]赤道半径
H [km]地表からの高度
V [km/s]速度
T [s]周期

$V[km/s] = (\frac{398600[km^3/s^2]}{(6378[km] + H[km])})^\frac{1}{2}$

$T[s] = \frac{2\pi(6378[km] + H[km])}{V[km/s]}$

(defn v [h]
  (Math/sqrt (/ 398600 (+ 6378 h))))

(defn t [h v]
  (/ (* 2 Math/PI (+ 6378 h)) v))

(defn h->t [h]
  (t h (v h)))
;; 静止軌道(35786km) で 23時間56分
user=> (h->t 35786)
86163.61830152525

user=> (/ *1 3600)
23.934338417090345

Racketのサンドボックス機能

Racketには評価時のリソースの利用を制限するサンドボックス機能があります。

このあたりを参照

使用可能なメモリを制限して式を評価するプログラムを書いてみます。

#lang racket
(require racket/sandbox)

(define e (parameterize ((sandbox-memory-limit 1)) ;; 1MByte
            (make-evaluator 'racket/base)))

;; 1KByteくらい (1MByte制限)
(printf "~s\n" (vector-length (e '(make-vector 1000))))
(flush-output)

;; 1MByteくらい (制限なし)
(printf "~s\n" (vector-length (make-vector 1000000)))
(flush-output)

;; 1MByteくらい (1MByte制限)
(printf "~s\n" (vector-length (e '(make-vector 1000000))))
(flush-output)
> racket test.rkt
1000
1000000
out of memory 
  context...:
   /usr/racket/collects/racket/sandbox.rkt:355:0: call-with-limits
   /usr/racket/collects/racket/sandbox.rkt:403:0: call-with-custodian-shutdown
   /usr/racket/collects/racket/private/more-scheme.rkt:146:2: call-with-break-parameterization
   /usr/racket/collects/racket/sandbox.rkt:760:5: loop

[メモ]C#で時刻文字列を扱う・XMLファイルを読み込む

C#で時刻文字列のパース・フォーマット指定して文字列に変換

using System;
using System.Globalization;

class Test
{
  public static void Main()
  {
    DateTime tmp = DateTime.Parse("2012-12-16T00:00:00Z");
    Console.WriteLine(TimeZoneInfo.ConvertTimeToUtc(tmp).ToString("yyyy/MM/dd HH:mm:ss"));
    return;
  }
}
2012/12/16 00:00:00

C#でXMLファイルをパース

// mono-csc xml.cs -r:System.Xml.Linq.dll
using System;
using System.Xml;
using System.Linq;
using System.Xml.Linq;
using System.Collections.Generic;

// test.xml
// <a>
//  <b>b1</b>
//  <b>b2</b>
//  <c>c1</c>
// </a>

class Test
{
  static void Main()
  {
    var doc = XElement.Load("test.xml", LoadOptions.SetLineInfo);
    var elems = 
      from x in doc.Descendants()
      select x;

    foreach(var e in elems)
      {
        var text = String.Format("{0}:{1}", ((IXmlLineInfo)e).LineNumber, e.Value);
        Console.WriteLine(text);
      }

    return;
  }
}
2:b1
3:b2
4:c1

Clojureのリテラルその他

リテラル
内容X(type 'X)
10進数2 => 2java.lang.Long
8進数010 => 8java.lang.Long
16進数0x10 => 16java.lang.Long
2進数2r10 => 2java.lang.Long
36進数36r10 => 36java.lang.Long
BigInt3N => 3Njava.lang.BigInt
小数1.1 => 1.1java.lang.Double
小数1.0E8 => 1.0E8java.lang.Double
BigDecimal2.2M => 2.2Mjava.math.BigDecimal
有理数22/7 => 22/7clojure.lang.Ratio
シンボルabc あいう !clojure.lang.Symbol
キーワード:a => :aclojure.lang.Keyword
キーワード::a => :user/aclojure.lang.Keyword
文字列"abc" "\t\n"java.lang.String
文字\a \あ \spacejava.lang.Character
リスト(1 2)clojure.lang.PersistentList
空リスト()PersistentList$EmptyList
ベクタ[1 2]clojure.lang.PersistentVector
マップ{:k1 :v1 :k2 :v2}clojure.lang.PersistentArrayMap
セット#{:a :b :c}clojure.lang.PersistentHashSet
nil (null)nilnil
truetruejava.lang.Boolean
falsefalsejava.lang.Boolean
正規表現#"\d{2}\w+"java.util.regex.Pattern
コンストラクタ#java.lang.Double[1.1]クラス・レコード・タイプのインスタンス


読み込み時に他のフォームに置き換えられる表現
内容X'X
Quote'a(quote a)
Deref@a(deref a)
typehint^String^{:tag String}
Var-quote#'a(var a)
無名関数#(…)(fn [args] (…))
S式コメント#_Xフォーム X をコメントとして扱う
Syntax-quote`(…)-
読み込み時の評価#=(+ 1 2)3

2012年12月13日木曜日

正規表現を利用して文字列を作成する

( Lisp Advent Calendar 2012 の13日目の記事です)

テストなどのために適当なデータを作成したいことがあります。
この時、データをランダムに作成できると、楽ができる上に 自分の考えていなかった入力パターンから問題を見つけることができて良さそうです。

作成したいデータが数値なら、疑似乱数生成用の関数(rand,randomなど)を使えば事足ります。
文字列を作成したい場合は、正規表現を利用すれば楽ができそうです。

Clojure には、正規表現からその正規表現にマッチするような文字列を作成してくれる re-rand という ライブラリがあります。

[re-rand "0.1.0"]
user> (require [re-rand :as r])

user> (repeatedly 3 #(r/re-rand #"\d{4}/\d{2}/\d{2} \d\d:\d\d:\d\d"))
("7511/27/85 13:37:43" "6728/87/88 96:68:15" "1536/63/37 90:23:02")

user> (repeatedly 3 #(r/re-rand #"[0-9零一二三四五六七八九]{1,10}"))
("六二零七" "1七8" "二三七八一2八1")

Common Lisp には・・・と思って調べて見ましたが、見つけることができませんでした。 仕方ないので自分で作りましょう。

幸い、一番大変そうな正規表現のパースは cl-ppcre の parse-string 関数で行えます。

(asdf:load-system :cl-ppcre)

(defpackage :random-string
  (:use :cl)
  (:export random-string
           *repeat-limit*
           *charset-every*
           *charset-digit*
           *charset-word*
           *charset-whitespace*))

(in-package :random-string)

(defvar *repeat-limit* 64)

(defvar *charset-everything*
  (lambda () (code-char (random 256) )))

(defvar *charset-digit* "0123456789")

(defvar *charset-word*
  (concatenate 'string
   "abcdefghijklmnopqrstuvwxyz"
   "ABCEFGHJIJKLMNOPQRSTUVWXYZ"
   "!\"#$%'()-=^~\\|[]{};:+*,.<>/?_"))

(defvar *charset-whitespace*
  (format nil "%c%c%c" #\space #\newline #\tab))

(defun one-of (obj)
  (if (functionp obj)
      (funcall obj)
      (let ((count (length obj)))
        (elt obj (random count)))))

(defun random-string (regexp)
  (generate (ppcre:parse-string regexp)))

(defmethod generate ((obj string))
  obj)

(defmethod generate ((obj character))
  (string obj))

(defmethod generate ((obj list))
  (generate/list (first obj) (rest obj)))

(defmethod generate ((obj (eql :digit-class)))
  (string (one-of *charset-digit*)))

(defmethod generate ((obj (eql :everything)))
  (string (one-of *charset-everything*)))

(defmethod generate ((obj (eql :whitespace-char-class)))
  (string (one-of *charset-whitespace*)))

(defmethod generate ((obj (eql :word-char-class)))
  (string (one-of *charset-word*)))

(defmethod generate/list ((cls (eql :sequence)) rest)
  (apply #'concatenate 'string (mapcar #'generate rest)))

(defmethod generate/list ((cls (eql :char-class)) rest)
  (string (one-of (mapcar #'generate rest))))

(defmethod generate/list ((cls (eql :range)) rest)
  (string
   (destructuring-bind (from to) rest
     (code-char
      (+ (char-code from)
         (random (1+ (- (char-code to) (char-code from)))))))))

(defmethod generate/list ((cls (eql :INVERTED-CHAR-CLASS)) rest)
  (error "unsupported expression: INVERTED-CHAR-CLASS"))

(defmethod generate/list ((cls (eql :greedy-repetition)) rest)
  (destructuring-bind (min max gen-cls) rest
    (let ((max (or max (+ min *repeat-limit*))))
      (apply #'concatenate 'string
             (loop :repeat (+ min (random (1+ (- max min))))
                :collect (generate gen-cls))))))

(defmethod generate/list ((cls (eql :register)) rest)
  (apply #'concatenate 'string (mapcar #'generate rest)))

(defmethod generate/list ((cls (eql :alternation)) rest)
  (generate (one-of rest)))

CL-USER> (random-string:random-string "b(an)+a")
"banananananananananananananananana"

CL-USER> (dotimes (_ 3)
           (print (random-string:random-string "[012]\\d{3}-((0[1-9])|(1[12]))-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")))
"2056-09-58T98:80:10Z" 
"2057-02-82T36:65:58Z" 
"1077-11-18T77:72:22Z"

CL-USER> (dotimes (_ 3)
           (print (random-string:random-string "[\\d零一二三四五六七八九]{1,5}")))

"二二四四" 
"五68" 
"二七"

文字集合の選択などで偏りが出てしまいますが、適当なデータを作成する程度なら十分利用できるかなぁ、と思います。

今回書いたコードでは、やりたいことの9割くらいを正規表現ライブラリのcl-ppcreが行ってくれています。
Lisp使いは「全部自分で書く」みたいなイメージがありますが、 leiningen や Quicklisp を使うとライブラリのインストールは簡単ですし、 やりたいことにマッチするライブラリがあったら、ありがたく使わせてもらいましょう。

2012年12月10日月曜日

Log4CLを使ってみる

( Common Lisp Libraries Advent Calendar の10日目の記事です )

最近やっと実感を得ましたが、(デバッグ)ログは大事ですね。 遠隔地にいるライブラリ製作者とやりとりする回数を減らせます。

ということで、Common Lispでログ出力するライブラリ、Log4CL を眺めてみます。

Log4CL は、CLikiの Current recommended libraries で紹介されています。
JavaのロギングライブラリであるLog4jを参考にして作られているようなので、Java使いの人にとっては使いやすいのかもしれません。

インストールは Quicklisp で行えます。

> (ql:quickload :log4cl)

正確な情報はgithubにあるREADME.mdおよびソースコードを読んで調べて頂くとして、 ざっと概要を説明してみます。

Log4CL は次の3つの構成要素から成ります。

  • ロガー : ログの出力先 (論理的)
  • アペンダ : ログの出力先(出力方法) (コンソール出力・ファイル出力など)
  • レイアウト : ログ出力の整形方法

論理的な出力先であるロガーに、実際の出力方法を定義したアペンダを対応づけ、 レイアウトで指定されたフォーマットでログを出力します。

ログレベルは、 fatal,error,warn,info,debug, user1 ~ user4, trace, user5 ~ user9 の15段階が指定できます。 ログレベルの名前に対応する出力関数(マクロ)が定義されています。

CL-USER> (log:fatal "fatal error")
[22:57:21] [fatal] <cl-user> - fatal error

CL-USER> (log:info "hello")
[22:57:51] [info] <cl-user> - hello

1文字の省略形も定義されています。

CL-USER> (log:i "helo")
[22:59:26] [info] <cl-user> - helo

CL-USER> (log:w "w")
[23:00:10] [warn] <cl-user> - w

「<cl-user>」 は現在のパッケージ名です。 ロガーを指定しない場合、デフォルトでカレントパッケージの名前がロガーとして利用されます。

ロガーの作成はmake-logger関数、あるいはmake関数で行えます。
ロガーは親子関係を持ちますが、キーワードシンボルを指定すると 親=カレントパッケージ名, 子=シンボル名 という 構造が作成されるようです。

ログ出力関数でロガーを指定する場合は、名前(シンボル)を直接指定するほうが短くて良さそうです。

CL-USER> (log:w (log:make-logger :child) "w")
[23:02:41] [warn] <cl-user:child> - w

CL-USER> (log:f (log:make-logger '(parent child)) "f")
[23:03:13] [fatal] <parent:child> - f

CL-USER> (log:e :child "e")
[23:04:17] [error] <cl-user:child> - e

ロガーが出力するログレベルを設定するには、 config 関数を使います。省略形は c です。
なお、親ロガーの設定を引き継ぐよう設定するには、:unsetを指定すれば良いようです。 デフォルトの設定は :unset です。

CL-USER> (log:debug "hoge")
; No value
CL-USER> (log:c :d)  ;; (log:config :debug) と同等
CL-USER> (log:debug "hoge")
[23:13:02] [debug] <cl-user> - hoge

ログレベルに対応する出力処理以外に、評価前の式と評価後の値を出力する expr 関数があります。 処理系が SBCL の場合は、どの関数の中でログが出力されたかまでロガー名に含めてくれるようです。

(defun foo (a)
           (flet ((bar (b)
                    (log:expr a b (+ a b))))
             (bar 10)))
(log:config :debug)

;; CCL
CL-USER> (foo 2)
[23:17:57] [debug] <cl-user> - A=2 B=10 (+ A B)=12 

;; SBCL
CL-USER> (foo 2)
[23:17:06] [debug] <cl-user:foo:bar> - A=2 B=10 (+ A B)=12 

その他、アペンダやレイアウトの対応づけや設定ファイルの読み込み等が可能ですが、 README.md 以上に有益なことを書ける気がしないので、ドキュメントにかかれていない アペンダの定義を行い、syslogにログを出力してみます。

(ql:quickload :cl-syslog)

;; アペンダを定義
(defclass syslog-appender (log4cl-impl:appender)
  ())

;; アペンダの出力方法を定義
(defmethod log4cl-impl:appender-do-append ((appender syslog-appender) logger level log-func)
  ;; 出力はcl-syslogに丸投げ。
  (cl-syslog:log "log4cl" :local7 :info
    (with-output-to-string (s)
      (log4cl-impl:layout-to-stream
       (slot-value appender 'log4cl-impl:layout) s logger level log-func))))

;; ロガーを作成
(defvar syslog-logger (log:make '(syslog)))

;; ロガーにアペンダを対応付ける
(log:add-appender syslog-logger (make-instance 'syslog-appender))

;; ログ出力
(log:i syslog-logger "from cl")
kurohuku@mypc:/var/log$ tail -1 syslog
Dec 10 23:49:36 mypc log4cl: INFO - from cl

Log4jと異なりデフォルトで定義されてるアペンダは少ないので、 コンソールやファイル以外に出力を行いたい場合は自分でアペンダを定義する必要がありますが、 expr関数(マクロ)などはデバッグに便利ですし、試しに使ってみると良いと思います。

なお、Quicklispで (ql:system-apropos "log") でライブラリを検索したところ、 他にもロギングライブラリっぽいものが見つかりました。

  • cl-log
  • hu.dwim.logger
  • log5
  • logv
  • (cl-syslog)
  • (irc-logger)

・・・見つかりましたが、数が多いので比較はしません。ごめんなさい。

2012年12月5日水曜日

はじめてのRacket(の#lang)

Lisp Reader Macro Advent Calendar 2012 の5日目の記事です。

この記事では、プログラミング言語 Racket を簡単に紹介してみます。

Racket は、Scheme(Lisp)をベースとしたプログラミング言語です。
WindowsでもLinuxでも実行できて、GUIアプリケーションも作れます。
もともとPLT Schemeという名前でしたが、色々あってRacketという名前になったようです。 (参考:http://racket-lang.org/new-name.html)
どうせならもっとググりやすい名前にしてほしかったなーと思わないでもないです。

インストール方法は公式サイトを見ていただくとして、 まず最初に典型的な入門プログラム、Hello Worldを書いてみましょう。

#lang algol60

begin
 printsln(`hello world');
end

hello.rktと言う名前でファイルに保存してコマンドラインから実行してみます。
コマンド名は racket です。引数としてファイル名を渡すと、プログラムを実行してくれます。

> racket hello.rkt
hello world

次はもう少し複雑な例として、フィボナッチ数を再帰処理で求めるプログラムを書いてみます。

#lang algol60

begin
  integer procedure fib(x);
    integer x;
  begin
    integer result;
    result := 0;
    if x = 0 then
      result := 0
    else
      begin
       if x = 1 then
         result := 1
       else
         result := fib(x - 1) + fib(x - 2)
      end;
    fib := result
  end;
  printnln(fib(10));
end

Lispは括弧が多いのが特徴の言語らしいですが、このプログラムの括弧の数はたったの10個。 案外少ないです。

> racket fib.rkt
55

以上、入門終わり。
「こんなのLisp(Scheme)じゃない」「algol60って書いてあるぢゃん」と思うかもしれませんが、 ちゃんと実行もできるRacketのプログラムです。

Racketにはソースコードの先頭にかかれた #lang foobar という1行で、 使用する言語を切り替える機能があります。

たとえば、ごく普通のRacketプログラムを書きたい場合は、次のようになります。

#lang racket
(printf "hello world\n")

先頭行の #lang racket は、初期状態としてracketモジュールをインポートした状態で プログラムを解釈してくれ、というような意味となります。

Racketではソースコードの読み込みを read や read-syntax といった関数で行っています。
そのため、#lang で指定された言語(モジュール)で定義されているread関数によっては、 最初のhello worldのようなLispとはかけ離れた見た目のプログラムも実行できるのです。

Racketインストール時についてくるalgol60 以外の言語としては、静的型付けな Typed Racket や

racket> (require typed/racket)
racket> (let ((a 0)) a)
- : Integer [generalized from Zero]
0
#lang typed/racket

;; typed-racket-test.rkt
(provide fib sum)

(: fib (Integer -> Integer))
(define (fib n)
  (cond
   ((<= n 0) 0)
   ((= n 1) 1)
   (else (+ (fib (- n 1)) (fib (- n 2))))))

(define: (sum [lst : (Listof Integer)]) : Integer
  (let loop ((rst lst) (acc 0))
    (if (null? rst)
        acc
        (loop (cdr rst) (ann (+ acc (car rst)) Integer)))))
racket> (load "typed-racket-test.rkt")
racket> (require (prefix-in t: 'typed-racket-test))
racket> (t:sum '(1 2 3))
6
racket> (t:fib 10)
55
#lang typed/racket

;; typed-racket-err.rkt
(provide sum average)

(define: (sum [lst : (Listof Integer)]) : Integer
  (let loop ((rst lst) (acc 0))
    (if (null? rst)
        acc
        (loop (cdr rst) (ann (+ acc (car rst)) Integer)))))

(define: (average [lst : (Listof Real)]) : Real
  ;; sum関数はIntegerのリストを想定しているが、ここではRealのリストが渡されている
  (/ (sum lst) (length lst)))
;; 型があっていないと、実行時ではなくロード時に怒られます。
 racket> (load "typed-racket-err.rkt")
 typed-racket-err.rkt:14:10: Type Checker: Expected (Listof Integer), but got (Listof Real)
  in: lst
  errortrace...:
  context...:
 /usr/racket/collects/typed-racket/typecheck/tc-toplevel.rkt:295:0: type-check
   success
 /usr/racket/collects/typed-racket/typed-racket.rkt:38:4
 /usr/racket/collects/errortrace/errortrace-lib.rkt:434:2: errortrace-annotate
 /usr/racket/collects/errortrace/errortrace-lib.rkt:480:4
 /usr/racket/collects/racket/private/misc.rkt:87:7

論理型言語 Datalog

#lang datalog

親(たかし, かあちゃん).
親(たけし, かあちゃん).
親(かあちゃん, ばあちゃん).
親(かあちゃん, じいちゃん).

先祖(A, B) :- 親(A, B).
先祖(A, B) :- 親(A, Z), 先祖(Z, B).
兄弟(A, B) :- 親(A, Z), 親(B, Z), A != B.

先祖(たけし, X)?

兄弟(たけし, X)?
> racket datalog-test.rkt 
先祖(たけし, かあちゃん).
先祖(たけし, じいちゃん).
先祖(たけし, ばあちゃん).
兄弟(たけし, たかし).

プレゼンのスライドを作成する言語 Slideshow といったものがあります。

#lang slideshow

;; 1枚目
(slide
 #:title "Title-1"
 (t "Hello"))

;; 2枚目
(slide
 #:title "Title-2"
 (item "1st")
 (item "2nd"))

他にも、 ドキュメント記述用の Scribble 、 遅延評価を行う Lazy Racket といった言語もあります。

自分で独自の言語を作りたい場合でも、read関数を自分で用意すればよいだけです。 parser-tools/lex や parser-tools/yacc を使えばコンパイラを作る講義も Racketだけで対応できそうです。

日本ではユーザーが少ないイメージのあるRacketですが、 読み込み処理以外にもいろいろいじれるので、試しに触って遊んでみると楽しいのではないでしょうか。