CL Cookbook - loop
CL Cookbookの和訳です。loopの章の和訳を掲載します。
背景
loopマクロは、Common Lispで価値が高いのにも関わらず、ドキュメントが十分に整っていません。なぜ価値が高いのかというと、loopマクロは強力で、コンパクトで、map系の関数や再帰のようなものと比べて読みやすいコードになるからです。loopマクロでは、他の伝統的なプログラミング言語に慣れたプログラマーに親しみやすいスタイルを使います。
ここでは、loopマクロの使い方について説明します。loopマクロは、その内部にCやPascalのような複雑なシンタックスを持っている点で、大半のLispプログラムと異なります。loopマクロで書かれたコードを読むとき、脳の半分をLispモード、もう半分をPascalモードで考える必要があります。
loopマクロには4つのパーツがあると考えてください:
1. 繰り返される変数を設定する式
2. 条件により反復を行う式
3. それぞれの反復に対して何かを行う式
4. loopが終了する前に行う式
さらに、loopマクロは、値を返すこともできます。
loopマクロを使うときに、これらの全てのパーツを使うことはあまりありませんが、それぞれを様々な方法で組み合わせて使うことができます。
このチュートリアルの出典先は、Tutorial for the Common Lisp Loop Macroです。 Peter Karp氏の許可をえて掲載しています。
例
リストを通して反復し、それぞれの要素を出力します。
~~~lisp * (loop for x in '(a b c d e) do (print x) )
A B C D E NIL ~~~
2つのリストを交互に反復して、loopに返される値をコンスセルにまとめます。
~~~lisp * (loop for x in '(a b c d e) for y in '(1 2 3 4 5) collect (list x y) )
*1 ~~~
カウンタと値を用いて反復します。ここでの値は、反復のたびに計算されます。
~~~lisp * (loop for x from 1 to 5 for y = (* x 2) collect y)
(2 4 6 8 10) ~~~
リストを通して反復し、カウンターを交互に反復します。リストの長さは、反復が終了したときに決まります。2つのアクションが定義されており、そのうち1つは条件つきで実行されます。
~~~lisp * (loop for x in '(a b c d e) for y from 1
when (> y 1)
do (format t ", ")
do (format t "~A" x)
)
A, B, C, D, E NIL ~~~
if節を用いてloopを進めることもできます。
~~~lisp * (loop for x in '(a b c d e) for y from 1
if (> y 1)
do (format t ", ~A" x)
else do (format t "~A" x)
)
A, B, C, D, E NIL ~~~
testを用いて、loopを早く実行しましょう。アクションは、任意数の行で構成できます。また、loopのレキシカルスコープの外で定義された変数も参照できます。
~~~lisp * (loop for x in '(a b c d e 1 2 3 4) until (numberp x) collect (list x 'foo))
*2 ~~~
"While"も実行するかのチェックに使うことができます。"do"と"collect"はどちらも、1つの式にまとめることができます。
~~~lisp * (loop for x from 1 for y = (* x 10) while (< y 100)
do (print (* x 5))
collect y)
5 10 15 20 25 30 35 40 45 (10 20 30 40 50 60 70 80 90) ~~~
loopは、様々な方法でネスト(入れ子)にすることが可能です。
~~~lisp * (loop for x from 1 to 10 collect (loop for y from 1 to x collect y) )
*3 ~~~
複数の変数は、複合的なリストの要素を通して、ループすることができます。
~~~lisp * (loop for (a b) in '*4 collect (list b a) )
*5 ~~~
"return"アクションは、loopを止めて、結果を返すことができます。ここでは、文字列のsにある最初の数字を返します。
~~~lisp * (let *6 (loop for i from 0 below (length s) for ch = (char s i) when (find ch "0123456789" :test #'eql) return ch) )
\4
~~~
when/returnの組み合わせを短縮して書くことができるアクションもあります。
lisp
* (loop for x in '(foo 2)
thereis (numberp x))
T
lisp
* (loop for x in '(foo 2)
never (numberp x))
NIL
lisp
* (loop for x in '(foo 2)
always (numberp x))
NIL
Parenscript チュートリアル
本記事は、原著者の許諾のもと、翻訳・掲載しています。
Parenscript Tutorial / Vladimir Sedach
Parenscript チュートリアル
はじめに
このチュートリアルでは、LispからJavaScriptへのコンパイラであるParenscriptを用いて、簡単なWebアプリを開発します。Parenscriptの関数とマクロの詳細は、Parenscript Reference Manualを参照してください。
プロジェクトを始める
まず、Common Lispの処理系をインストールしましょう。SBCLは良い処理系です。
Common Lisp処理系の一覧は、CLikiのCommon Lisp処理系一覧に記載されています。 次に、パッケージメネージャーのQuicklispをインストールしましょう。
このチュートリアルでは、以下のライブラリを使います:
CL-FAD ファイル関連のユーティリティー CL-WHO HTMLジェネレーター Hunchentoot webサーバ Parenscript JavaScriptジェネレーター
Quicklispで読み込みましょう:
(mapc #'ql:quickload '(:cl-fad :cl-who :hunchentoot :parenscript))
次に、チュートリアルのコードを収納するためのパッケージを定義しましょう:
(defpackage "PS-TUTORIAL" (:use "COMMON-LISP" "HUNCHENTOOT" "CL-WHO" "PARENSCRIPT" "CL-FAD")) (in-package "PS-TUTORIAL")
CL-WHOでは、HTML属性をエスケープするかどうかを、自分で決めることができます。JavaScriptのインラインコード内でクオートされた文字列が、HTML属性の中で正しく動作するには、HTML属性にはダブルクォートを使い、JavaScriptの文字列にはシングルクォートを使うことで対処できます。
(setq cl-who:*attribute-quote-char* #\")
では、サーバを起動しましょう:
(start (make-instance 'easy-acceptor :port 8080))
例
psマクロは、S式でParenscriptのコードを受けとります。Parenscriptのコードは、Common Lispと同じように表現でき、マクロ展開時に、JavaScriptコードを含む文字列に可能な範囲で変換されます。
(define-easy-handler (example1 :uri "/example1") () (with-html-output-to-string (s) (:html (:head (:title "Parenscript tutorial: 1st example")) (:body (:h2 "Parenscript tutorial: 1st example") "Please click the link below." :br (:a :href "#" :onclick (ps (alert "Hello World")) "Hello World")))))
WebページにParenscriptのコードを含めるには、HTMLタグにコードを埋め込みます:
(define-easy-handler (example2 :uri "/example2") () (with-html-output-to-string (s) (:html (:head (:title "Parenscript tutorial: 2nd example") (:script :type "text/javascript" (str (ps (defun greeting-callback () (alert "Hello World")))))) (:body (:h2 "Parenscript tutorial: 2nd example") (:a :href "#" :onclick (ps (greeting-callback)) "Hello World")))))
別の方法としては、生成されたJavaScriptのコードを、別のHTTPリソースとしてサーブする(serve)ことでも可能です。このリソースへのリクエストは、ブラウザ内でキャッシュできます:
(define-easy-handler (example3 :uri "/example3.js") () (setf (content-type*) "text/javascript") (ps (defun greeting-callback () (alert "Hello World"))))
スライドショー
次に、より複雑な例に挑戦しましょう。スライドショーで画像を閲覧するアプリです。
まず、スライドショーを定義する方法が必要です。このチュートリアルでは、画像ファイルを含むフォルダを用意して、フォルダーごとのURLに基づいて、それぞれのスライドショーを提供します。/slideshows/{スライドの名前}
でスライドショーを提供するために、独自のHunchentootハンドラを使います。また、/slideshow-images/{スライドショーの名前}/{画像のファイル名}
にある画像をサーブするために、Hunchentootのfolder dispatcher関数を使います。
(defvar *slideshows* (make-hash-table :test 'equalp)) (defun add-slideshow (slideshow-name image-folder) (setf (gethash slideshow-name *slideshows*) image-folder) (push (create-folder-dispatcher-and-handler (format nil "/slideshow-images/~a/" slideshow-name) image-folder) *dispatch-table*))
マシン上で写真を探し出し、Hunchentootに写真をサーブさせましょう:
(add-slideshow "lolcat" "/home/junk/lolcats/") (add-slideshow "lolrus" "/home/other-junk/lolruses/")
次に、スライドショーのWebページを作ります。JavaScriptを使うと、ページ全体を再読み込みせずに、スライドショーを見ることができます。また、JavaScpiptを有効にしていないブラウザでも通常のリンク移動を提供できます。どちらにせよ、スライドショーの視聴者に、スライドショーのシーケンス内での位置をブックマークしておきたいはずです。
サーバとブラウザ両方に、スライドショーの画像のための URIを生成する方法が必要です。defmacro+psマクロ
は、Common LispとParenscriptの間でのマクロ定義を共有します。defmacro+psマクロ
を使うと、コードの重複を省けます。
(defmacro+ps slideshow-image-uri (slideshow-name image-file) `(concatenate 'string "/slideshow-images/" ,slideshow-name "/" ,image-file))
次に、スライドショーのページをサーブするための関数を定義しましょう。ページは、/slideshow-images/{スライドショーの名前}
のURIで画像をサーブされます。全てのページは、{スライドショーの名前}
でディスパッチされる単独の関数によって処理されます。
JavaScriptを有効にしているブラウザは、インラインスクリプト内のps*関数によって生成されたスライドショーに関する情報をえることができます。ps*関数は、ランタイム時に生成されたコードを変換するために使われます。スライドショーの移動は、onclickハンドラで実行されます。onclickハンドラは、コンパイル時に、psマクロによって生成されます。
通常のHTMLのスライドショー移動は、クエリパラメータを用いて行われます。
(defun slideshow-handler () (cl-ppcre:register-groups-bind (slideshow-name) ("/slideshows/(.*)" (script-name*)) (let* ((images (mapcar (lambda (i) (url-encode (file-namestring i))) (list-directory (or (gethash slideshow-name *slideshows*) (progn (setf (return-code*) 404) (return-from slideshow-handler)))))) (current-image-index (or (position (url-encode (or (get-parameter "image") "")) images :test #'equalp) 0)) (previous-image-index (max 0 (1- current-image-index))) (next-image-index (min (1- (length images)) (1+ current-image-index)))) (with-html-output-to-string (s) (:html (:head (:title "Parenscript slideshow") (:script :type "text/javascript" (str (ps* `(progn (var *slideshow-name* ,slideshow-name) (var *images* (array ,@images)) (var *current-image-index* ,current-image-index))))) (:script :type "text/javascript" :src "/slideshow.js")) (:body (:div :id "slideshow-container" :style "width:100%;text-align:center" (:img :id "slideshow-img-object" :src (slideshow-image-uri slideshow-name (elt images current-image-index))) :br (:a :href (format nil "/slideshows/~a?image=~a" slideshow-name (elt images previous-image-index)) :onclick (ps (previous-image) (return false)) "Previous") " " (:a :href (format nil "/slideshows/~a?image=~a" slideshow-name (elt images next-image-index)) :onclick (ps (next-image) (return false)) "Next"))))))))
この関数は独自のハンドラなので、スライドショーのために新たにディスパッチャーを作る必要があります。関数オブジェクトの代わりに、ハンドラを命名するシンボルを渡す必要があります。そうすることで、ディスパッチャーに触れずに、ハンドラを再定義することができます。
(push (create-prefix-dispatcher "/slideshows/" 'slideshow-handler) *dispatch-table*)
最後に、/slideshow.js
を定義しましょう。
(define-easy-handler (js-slideshow :uri "/slideshow.js") () (setf (content-type*) "text/javascript") (ps (define-symbol-macro fragment-identifier (@ window location hash)) (defun show-image-number (image-index) (let ((image-name (aref *images* (setf *current-image-index* image-index)))) (setf (chain document (get-element-by-id "slideshow-img-object") src) (slideshow-image-uri *slideshow-name* image-name) fragment-identifier image-name))) (defun previous-image () (when (> *current-image-index* 0) (show-image-number (1- *current-image-index*)))) (defun next-image () (when (< *current-image-index* (1- (getprop *images* 'length))) (show-image-number (1+ *current-image-index*)))) ;; use fragment identifiers to allow bookmarking (setf (getprop window 'onload) (lambda () (when fragment-identifier (let ((image-name (chain fragment-identifier (slice 1)))) (dotimes (i (length *images*)) (when (string= image-name (aref *images* i)) (show-image-number i)))))))))
@と chainプロパティーは、 便利なマクロにアクセスします。(@ object slotA slotB)
は、(getprop (getprop object 'slotA) 'slotB)
に展開されます。chain
も似ていますが、ネストされたメソッド呼び出しを提供します。
作者: Vladimir Sedach <vsedach@oneofus.la> 最終更新日: 2018-03-28
- 訳者は、macOSでチュートリアルを追試しました。ホームディレクトリに"junk/lolcats/"というディレクトリを作成して、jpeg形式で猫の画像を入れました。
- スライドショーにアクセスするには、http://localhost:8080/slideshows/lolcatからアクセスしました。
CL Cookbook - パッケージ
---
原文は、[The Common Lisp Cookbook – Packages](http://lispcookbook.github.io/cl-cookbook/packages.html)です。g000001さんからご助言をいただき、`do-symbols`マクロについて加筆しました。
---
# パッケージにあるシンボルを列挙する
Common Lispには、パッケージ内のシンボルをまとめて把握するためのマクロがいくつかあります。特に興味深いのは、[`do-symbols`と`do-external-symbols`][do-sym]です。
`do-symbols`は、 該当するパッケージでアクセスできる全てのシンボルを列挙します。例えば、:clパッケージにある全てのシンボルを表示するには、次のように書きます。
~~~lisp
(do-symbols (s :cl) (print s))
~~~
`DO-EXTERNAL-SYMBOLS`は、exportされたシンボルだけを列挙します。"PACKAGE"という名前のパッケージでexportされた全てのシンボルを表示するには、次のように書きます。
~~~lisp
(do-external-symbols (s (find-package "PACKAGE"))
(print s))
~~~
また、リスト形式でシンボルを集めて表示するには、次のように書きます。
~~~lisp
(let (symbols)
(do-external-symbols (s (find-package "PACKAGE"))
(push s symbols))
symbols)
~~~
もしくは、[`LOOP`][loop]マクロを使って、次のように書くこともできます。
~~~lisp
(loop for s being the external-symbols of (find-package "PACKAGE")
collect s)
~~~
[guide]: http://www.flownet.com/gat/packages.pdf
[do-sym]: http://www.lispworks.com/documentation/HyperSpec/Body/m_do_sym.htm
[loop]: http://www.lispworks.com/documentation/HyperSpec/Body/06_a.htm
Node.js入門: 導入 - Node.jsでアプリケーションを書くための入門ガイド
Node.js入門: 導入
Node.jsでアプリケーションを書くための入門ガイド
本記事は、原著者の許諾のもと、翻訳・掲載しています。
Getting started with Node.js: Introduction / Nilesh Singh
1989年、Tim Berners-Lee氏が、今日、インターネットとして世に知られているビジョンを提案しました。当時、その提案は十分に受け入れられませんでしたが、彼はWebの初期バージョンを開発する時間を与えられることになり、その結果、HTML、URI、HTTP、ブラウザ(WorldWideWeb)が、初のWebサーバhttpdと共に公開されました。
それ以降、その提案は我々にとって必要不可欠になり、httpdサーバは、今日、Webサービスが書かれる際の中心的なアイデアになりました。
では、サーバとはどのようなもので、なぜWebの設計において必要なのでしょうか。Wikipediaでは、サーバについて、次のように書かれています:
サーバとは、「クライアント」と呼ばれる他のプログラムや機器に、機能を提供するコンピュータプログラムか機器である。
サーバとは、リモートコンピュータであり、ユーザのリクエストを受けて、その結果を返すものです。では、サーバにはどのようなリクエストが入ってきて、また、どのように受け入れるのでしょうか?この部分が、サーバサイドプログラミングが扱うところです。我々はこれまで、Java、Ruby on Rails、Django、PHP、ASP.NET、Apache HTTP Server等を、ビジネスにで用いてきました。それらは、期待にそぐわないと見なされるまでは、すごく良いものでした。
これらのフレームワークは、用いられる目的において優れており、産業界でも広くシェアを獲得してきました。しかし、インターネットが進化するにつれて、よりダイナミックなサーバが必要とされるようになりました。
クライアント側のアプリケーションが数百マイクロ秒ごとにコンテンツを更新するように求めるような、長いポーリングを扱うアプリケーションを作ると仮定しましょう。
先ほどのテクノロジーを用いると、クライアントがリクエストを停止するのを確認するまでスレッドが残るので、スレッドが作られるごとに機能は低下します。
これは、1秒間に数千の新しいクライアントを処理するシステムには対処できるようにみえますが、C10K問題が起こると、サーバは対処できなくなります。
複数の機器を使うと、最近のコンテンツのために、サーバにポーリングをすることができます。
Node.jsやNginxのようなイベント駆動の設計では、C10K問題を扱うのにより効率的です。実際、どちらも、その問題を解決するために開発されました。では、Node.jsとはどういうものであり、何ができて、どのように実行しているのかに焦点をあてて見ていきましょう。Nginxについては、後の記事で紹介します。
結局、Node.jsとは何なのか?
Node.jsは、2009年はじめに、Ryan Dahlさんに書かれました。Ryanさんは当時、大量のリクエストを同時に処理できないこと、また、実行ブロックスタイル(逐次プログラミング)を用いる既存のWebサーバに対して批判的でした。Ryanさんは、並行性を念頭に、Google ChromeのオープンソースのJavaScriptエンジンV8とイベントループを用いてNode.jsを書きました。V8とイベントループを組み合わせたことで、Node.jsは、複数の同時リクエストを、シングルスレッド上でI/Oブロッキングなしで処理できるようになりました。しかし、どのようにして実現できたのでしょうか?
イベントループを理解する
Node.jsにおいて、全ては非同期のタスクです。入力スクリプトの初期化からシステムのコールバックにいたるまで、最初から全ては非同期であり、あるタスクが終了しても失敗しても発動(fire)します。
イベントループは、主に、シングルスレッドのJavaScript環境をシステムのカーネルに引き渡すために機能します。
最近の大半のOSは、非同期のタスクを渡すためのカーネルインターフェイスを提供していますが、提供していないOSもあります。そのような場合は、Node.jsにイベントループを提供するLibevというライブラリを使います。Libevは、非同期のI/Oタスクを渡すために、4つのスレッドを作ります。
Node.js内部の働きについて興味があるようでしたら、このチュートリアルは役にたつでしょう。イベントループとその内部の振る舞いについては、別の記事で紹介します。
Node.jsでサーバを起動する
簡単なNode.jsアプリケーションを書くには、すべきことが2つあります。1つ目は、Node.jsが提供するHTTPモジュールをインポートすることです。JavaScript共通のrequireメソッド、もしくは、ECMAScriptのimportを使ってモジュールをインポートした後、createServerメソッドでサーバを初期化する必要があります。createServerメソッドは、コールバック関数を、引数、リクエスト、レスポンスと一緒に登録します。
const http = require('http')
const server = http.createServer((req, res) => { res.setHeader('Content-Type', 'text/plain') res.end('Hello, this is a sample server!') })
server.listen(8080)
先ほども言及した通り、Node.jsにおいては全てのコードは、非同期のタスクです。createServerメソッドがイベントループと一緒に登録したコールバック関数は、新しいリクエストがサーバにヒットする度に呼び出されます。そして、リクエストオブジェクトとレスポンスオブジェクトは、スレッドプールかOSカーネルの非同期のインターフェイスによって準備されます。
コールバックの後に続くことは単純明快です。リクエストオブジェクト(req)を受けとり、求められる詳細を引き出し、要求に応じて、レスポンスオブジェクト(res)をリクエストの送り主(クライアント)に返します。本当にそんなに単純なのでしょうか?では、これらのコールバックオブジェクトについて見ていきましょう。
リクエストオブジェクトを理解する
リクエストは、その名前が示すように、サーバにヒットする全てのリクエストを処理するハンドラです。reqは、ただのJavaScriptオブジェクトであり、サーバにリクエストとして認められるのに必要な情報(クライアントのIPアドレス、ホスト名、ヘッダ、ボディー部(JSON等の形式)、パラメータ、クエリ文字等)を含みます。
# リクエストのURLに含まれるパラメータを読む const params = req.params
リクエストのプロパティを正しくパースして、適切にレスポンスを返すのは、サーバの責任です。Node.jsはリクエストをパースしますが、リクエストオブジェクトを読み、リクエスト-レスポンスのトランザクションを終わらせるのは、プログラマーの仕事です。
リスポンスオブジェクト
トランザクションを完了するために、有効なレスポンスをユーザに送る必要があります。しかし、何が有効で、何が有効でないのでしょうか?また、どのように、また何をレスポンスとして返すのでしょうか?
Node.jsは、そのような場面を対処するために、最低限のAPIを提供しています。リクエストのコールバックとして受け取るレスポンスオブジェクトは、クライアントに応答するために必要な全てのメソッドを保持します。レスポンスヘッダーを設定するためのsetHeader、データをレスポンスのボディー部にアタッチするために用いられるappend、また、レスポンスのプロセスを終わらせるためのend等のメソッドを含みます。
res.setHeader('Content-Type', 'text/plain')
事前に定義されたメソッドを用いることは必ずしも有効であるとは限りませんが、それは確かにエラーを最小限に留めてくれます。
結局、何を受け入れるのか?
最後のパズルのピースは、listenメソッドです。サーバのインスタンスを初期化した後、なぜlistenメソッドが必要なのでしょうか。
ネットワークの授業を受けたことがあれば、クライアントは任意である一方で、サーバーは全てのクライアントにIPアドレスが知られている1つのシステムだと知っているでしょう。全てのクライアントに知らせるために、サーバはlistenとacceptの仕組みを実装します。一度リクエストが確立されると、クエリに保持されて、サーバはアクセプトする準備ができます。
listenメソッドは Node.jsでlistenの部分を担い、createServercallbackは acceptの部分を処理します。listenは4つのパラメータをうけとります。1つ目はNode.jsが動いているポート番号です。2つ目はホスト名であり、サーバ内のプロセスを特定するためのポート番号を伴います。
const PORT = 8080, HOSTNAME = '127.0.0.1', BACKLOG = 10 server.listen(PORT, HOSTNAME, BACKLOG, () => { console.log('Server is now listening on', PORT) })
3つ目とパラメータは、リスナーのクエリを保留するためのbacklogです。4つ目のパラメータは、リスナーが最初に成功したときに呼び出されるコールバック関数です。
全てのパラメータのlistenの受け入れはオプショナルですが、常にポート番号とホスト名、またbacklogも割り当てておくことが良いです。理由は、そのデフォルト値にあります。もしOSが独自のポートを見つけられない場合や、0に設定された場合、OSは使用されていないポート番号をプロセスに割り当てます。同様のことがホスト名にも当てはまります。もしサーバが独自のホスト名を見つけられない場合、IPv4では(0.0.0.0)、IPv6では(::)で接続のアクセプトを開始します。どちらの値も、プロキシサーバの後ろで動作させるケースや、80番ポートで起動させるといった運用環境では受け付けられません。
上のコードを用いると、どのようなリクエストに対しても、"Hello, this is a sample server!"という文字列を返すサーバを起動することができます。 上のコードを(server.js)という名前で保存して、次のように起動してください:
node server.js
起動すると、リスナーのコールバック関数で定義したように、"Server is now listening on 8080"というログが表示されるでしょう。Node.jsで優れている点は、listenするために、CPUのリソースが食いつぶされないことでしょう。新しいリクエストが処理されるまでの間、じっと休止しています。
では、ここまでのNode.js入門をまとめましょう。Node.jsとは何なのか、またNode.jsがスレッド対応に至った背景、イベントループについて、HTTPサーバの起動方法について見てきました。Node.jsでのリクエストとレスポンスのオブジェクトについても学びました。また、Node.jsが、イベントループの助けをかりて、新しいリクエストをlisten, acceptして、リクエスト-レスポンスんのトランザクションを完了しているかを見てきました。
もし、この記事について、何か問題や質問があれば、下のコメントからお願いします。あなたからのクエリに喜んでリスポンドします。また、もしこの記事が役にたつと思ったら、星ボタンを押していただき、周りの人に勧めて共有してください。
ClojureScriptでコマンドライン・スクリプトを書く
本記事は、原著者の許諾のもと、翻訳・掲載しています。 Command line scripting with ClojureScript / akiroz
ClojureScriptコミュニティによる開発のおかげで、コマンドライン・スクリプトをClojureで書くのが楽しくなってきました。ClojureScriptの中心チームとlumoを開発しているanmonteiroさんには、心から敬意を表します。
Clojureは、データを処理するための短いスクリプトを書くのに良い言語だと思います。操作用の関数やイミュータブルな構造が言語に組み込まれているので、参照性やdeep-cloningについて心配する必要がありません。
Lumoでスクリプトを起動する
では、始めるために簡単な方法を紹介します。lumoをシステムにインストールして、Clojureのファイルを起動します。hello.cljs
という名前でファイルを作り、次のように編集して保存してください。
(println "Hello World!")
では、次のように実行してみましょう。
$ npm i -g lumo-cljs # lumoをインストールする $ lumo hello.cljs Hello World!
簡単でしょう? では次は、より実用的なプログラムを書くために、Node.jsのAPIを利用する方法を紹介します。
;; 次のプログラムでは、https://randomuser.meで生成されたjsonのペイロードをパーズして、 ;; データの一部を抽出します。予め、次のコマンドでデータをダウンロードしてください。 ;; $ wget 'https://randomuser.me/api/?results=10' -O randomUsers.json ;; 注: シングルクォートをお忘れなく (require '[clojure.string :refer [capitalize]] ;; Clojureのライブラリをrequireします '[fs :refer [writeFileSync]] ;; Node.JSのfilesystem関連のモジュールをrequireします '["./randomUsers.json" :as input-json] ;; JSONファイルをJSのデータとしてrequireします ) (defn parse-user [{:keys [email name] {:keys [username password]} :login}] {:id (str (random-uuid)) ;; built-in :username username :password password :email email :full-name (str (capitalize (:first name)) " " (capitalize (:last name))) }) (let [results (:results (js->clj input-json :keywordize-keys true))] (writeFileSync "randomUsers.edn" (pr-str (mapv parse-user results))))
ご覧の通り、このrequire関数は、JSのrequire関数と同じように動作します。次のようにすることと同じです。
const { writeFileSync } = require('fs'); const inputJson = require('./randomUsers.json');
npmからインストールされるnode_modulesにあるモジュールとも上手く動作します(現状lumo1.8.0)。残念ながら、Clojure側の依存関係を管理する簡単な方法はありません。現状は、Clojureのライブラリが収められるjarファイルを手動で管理する必要があります。詳しくは、lumoのWikiを参考にしてください。
npmとの連携
少し大きなプロジェクトのためには、npmで依存関係を管理するために、package.jsonを使いたいでしょう。 もしlumoをグローバル環境にインストールしたくなければ、lumoをプロジェクト内だけにインストールすることもできます。
次の例では、上の例と同様に動作します。NPMにあるrequestのライブラリを使ってJSONをhttps://randomuser.me から取得します。先ほどのコードを、適切に名前空間をつけて、2つのClojureScriptのファイルに分割します。
my-tool |\_ package.json \_ src \_ my_tool |\_ core.cljs \_ user.cljs
Clojureの名前空間のシステムは、ディレクトリ構造を反映するので、my-tool.core
は、my_tool/core.cljs
でなければいけません。名前空間でハイフンで区切られた名前は、ファイルシステムではsnake_caseに変換されなければいけません。
core.cljs:
(ns my-tool.core (:require [my-tool.user :as user] [fs :refer [writeFileSync]] ;; request library from npm, since the imported name ;; is the same as the package name, :as isn't needed. ;; const request = require('request'); [request])) ;; The -main function is called by lumo with CLI args (defn -main [n] (request #js{:url (str "https://randomuser.me/api/?results=" n) :json true} (fn [_ _ body] (let [results (:results (js->clj body :keywordize-keys true))] (writeFileSync "randomUsers.edn" (pr-str (mapv user/parse results)))))))
user.cljs:
(ns my-tool.user (:require [clojure.string :refer [capitalize]])) (defn parse [{:keys [email name] {:keys [username password]} :login }] {:id (str (random-uuid)) :username username :password password :email email :full-name (str (capitalize (:first name)) " " (capitalize (:last name))) })
package.json:
{ "name": "my-tool", "version": "1.0.0", "scripts": { "start": "lumo -c src -m my-tool.core" }, "dependencies": { "lumo-cljs": "^1.8.0", "request": "^2.85.0" } }
package.json
内の"script"の行をご覧ください。
-cフラグは、lumoに対して、あなたのソースコードがどこにあるのかを知らせます。
-mフラグは、あなたの-main関数がどの名詞空間にあるのかを特定します。
ではこのツールをnpmを使って起動してみましょう。
$ npm install $ npm start 12 ## 12人のユーザを取得して、randomUsers.ednを出力します
REPLでの開発
Clojureを経験するのに、REPLを元にしたインタラクティブな開発なしには終われません。
package.json
のscripts
セクションに次の行を足してください。
"repl": "lumo -c src -i src/my_tool/core.cljs -n 5777 -r"
-iフラグは、core.cljs
を起点にREPLを初期化します。
-nフラグは、portを5777番でソケットのREPLを開始します。
-rフラグは、ターミナルでREPLを起動します。
このようにしてREPLを開始することで、状態(state)を失うことなく、任意のコードをあなたのランタイムで実行できます。
$ npm run repl ... cljs.user=> (in-ns 'my-tool.core) ;; switch to our core namespace my-tool.core=> (user/parse {:name {:first "john" :last "smith"}}) {:id "c1b61773-133e-434c-afbd-d82b95b814d3", :username nil, :password nil, :email nil, :full-name "John Smith"}
さいごに
現状でのツールには荒い点がいくつかありますが、Clojureでコマンドラインスクリプトを書き始めるのも、選択肢の1つにあげてもいい時期にきています。Lumoの起動時間は、Clojureが起動する時間と比べてはるかに早いので、新鮮な感じがすると思います。
もしClojureが好きならば、この記事の内容を試してみてください。
Radiance - Common Lisp Webアプリケーション開発環境
原著者の許諾のもと、翻訳・掲載しています。
Radiance: A Common Lisp web application environment , written by Shirakumo
Radianceについて
Radianceは、Webアプリケーションの開発環境です。Webフレームワークの一種ですが、汎用的で、変更も容易です。 特別な変更を加える必要なく、WebサイトやWebアプリを簡単に書くことができます。
入手するには
Radiance、また関連するモジュールやアプリケーションは、Quicklispを通して配布されています。Radianceをインストールするには次のようにしてください。
(ql-dist:install-dist "http://dist.tymoon.eu/shirakumo.txt") (ql:quickload :radiance)
以上で、Quicklispのquickload
コマンドで、Purplish等のRadianceモジュールを利用できるようになります。
チュートリアル
Radianceのチュートリアルはこちらです。Radianceの重要コンセプトや、Webアプリの書き方を紹介しています。チュートリアルを通して、Radianceの使い方に慣れることができます。また、特定の機能が必要なときに、どこを調べるべきかが分かります。チュートリアルの最後では、本番環境でのRadianceのインストールとデプロイ方法について説明しています。
簡単な例
あなたが一番したいことは、ユーザにHTMLを提供することでしょう。その方向に進めながら、少しずつ拡張していきます。まずは、Radianceの準備をします。
(ql:quickload :radiance) (radiance:startup)
初めてRadianceを使う場合には、r-welcome
モジュールを使ってください。r-welcome
モジュールを使うと、ブラウザーでの最初のページにリンクさせることができます。まずは、小さなページにリンクをはりましょう。
(in-package :rad-user) (define-page example "/example" () (setf (content-type *response*) "text/plain") "Hi!")
localhost:8080/exampleにアクセスすると、"Hi"と表示されるはずです。これでは、かなり退屈ですね。では、cl-whoを用いてHTMLを出してみましょう。
(ql:quickload :cl-who) (define-page example "/example" () (cl-who:with-html-output-to-string (o) (cl-who:htm (:html (:head (:title "Example Page")) (:body (:header (:h1 "Couldn't Be Simpler.")) (:main (:p "Trust me on this one.")))))))
普通であれば、フォントの書式を変えたり、CSSファイルを追加しながら、ページのスタイルを作っていきます。CSSファイルをサーブすることは可能ですが、長い目でみれば、最適の方法とはいえません。
ここでは、モジュール(module)を作る方法をご紹介します。モジュールを使うことで、Webページを適切に整理することができます。モジュールに必要なファイルは手動でも作れますが、ここでは雛形でモジュールを自動生成します。
(create-module "example")
モジュールが自動生成されたあと、モジュールへのパスが返されます。生成されるものは、ASDFのシステムファイル、lispのmainファイル、2つのフォルダ(static
とtemplate
)です。
static
フォルダには、静的にサーブされるファイルが入ります。template
には、テンプレートエンジン関連のファイルが入ります。
example.lisp
を開いて、先ほどのコードを元にページを定義しましょう。
(define-page example "/example" () (cl-who:with-html-output-to-string (o) (cl-who:htm (:html (:head (:title "Example Page")) (:body (:header (:h1 "Couldn't Be Simpler.")) (:main (:p "Trust me on this one.")))))))
ページは、シンボル名によって特定されます。自分のモジュールを作ると、パッケージが生成されます。前の例のシンボルは、一度も使われていないシンボルです。名前衝突を避けるために、rad-user
パッケージでページを消す必要があるかもしれません。
(remove-page 'rad-user::example)
次に、簡単なCSSファイルを作りましょう。static
フォルダに、example.css
という名前で作成してください。自分でCSSを書くのが面倒な方は、次のCSSコードをお使いください。
body{ font-family: sans-serif; font-size: 12pt; background: #EEE; } header{ text-align: center; } main{ width: 800px; margin: 0 auto 0 auto; background: #FFF; padding: 10px; border: 1px solid #BBB; border-radius: 5px; }
では、CSSファイルにリンクするように、HTMLを修正しましょう。アドレスがスタイルシートにたどり着くために、後の章ではルーティングシステムを使いますが、今はその必要はありません。焦らずに、進めていきましょう。
(define-page example "/example" () (cl-who:with-html-output-to-string (o) (cl-who:htm (:html (:head (:title "Example Page") (:link :rel "stylesheet" :type "text/css" :href (uri-to-url "/static/example/example.css" :representation :external))) (:body (:header (:h1 "Couldn't Be Simpler.")) (:main (:p "Trust me on this one.")))))))
ページを再度読み込むと、スタイルが適用されているはずです。uri-to-url
が動作する原理については、後ほど詳しく説明します。大切なことは、どのようにセットアップしても、静的ファイルへのリンクは適切に解決されるということです。
1. Radianceのコンセプトと部品
1.1 URI
Radianceの中心的なコンセプトはURI
です。URIはオブジェクトであり、ドメイン
、ポート番号(オプション)
、パス
を含むリストで構成されます。
RadianceのURIは、一般的なURIから要素を抽出したもので、スキーマ、クエリ、フラグメント等は含みません。また重要な違いとして、domains
のURIは、フレームワークにおいて、複数の場所で使われます。例えば、location
を捕捉するときや、dispatch matching
を処理するために使われます。
URIは変更可能です。URIの修正は、クリティカルパスがある場所で行われるので、パフォーマンス上、重要です。想定外の方法でURIを修正した場合、予期しない動作が発生する可能性があります。
URIは、単一の文字列で表現されます。文字列にシリアライズされて、完全なURIオブジェクトにパーズして戻すことも可能です。URIはFASLファイルにリテラルで書き出されるので、マクロから吐き出してもいいです。URIのシンタックスは、次の通りです。
URI ::= DOMAINS? (':' PORT)? '/' PATH? DOMAINS ::= DOMAIN ('.' DOMAIN)* DOMAIN ::= ('a'..'Z' | '0'..'9' | '-') PORT ::= ('0'..'9'){1, 5} PATH ::= .*
URIをURLに変換するために、uri-to-url
を使います。uri-to-url
を使うと、反転(reversal)、エンコーディング、フォーマットの修正は自動で行われます。
uri
, domains
, port
, path
, matcher
, uri-string
, make-uri
, make-url
, ensure-uri
, copy-uri
, parse-uri
, uri<
, uri>
, uri=
, uri-matches
, merge-uris
, represent-uri
, uri-to-url
を参考にしてください。
1.2 リクエストとレスポンス
行き来するデータを格納しておくために、request
オブジェクトとresponse
オブジェクトがあります。request
オブジェクトは、リクエストがどこに向かうのかを表すURI、POST、GET、ヘッダ、クッキーなどのHTTP通信のペイロードデータを全て保持します。response
オブジェクトは、リターンコード、ヘッダー、クッキー、BODYデータを保持します。
リクエストが行われている間、これらの2つのオブジェクトが必ず存在し、*request*
と*response*
に束縛されなければいけません。それらは、動的なページを生成するために必要な情報を多く保持しています。さらに、リクエストはdata
テーブルを含んでおり、任意のデータを保持することができます。これは、システム内の各々の部品の間で、リクエストの実行中に取得されるような情報をやりとりするときに役立ちます。
リクエストは、必ずしもHTTPサーバから来る必要はありません。動作をテストするために、プログラムからリクエストを送ることも可能です。どのような場合であっても、リクエストをディスパッチするインターフェイスはrequest
と呼ばれます。この仕組みは、リクエストとレスポンスを構築して、URIを適切に処理します。もし自分自身でリクエストオブジェクトを送りたいのであれば、execute-request
を使うこともできます。
リクエスト処理に関する詳しい情報は、dispatcher、pages、API endpointをご参照ください。
*request*
, *response*
, *default-external-format*
, *default-content-type*
, request
, uri
, http-method
, headers
, post-data
, get-data
, cookies
, user-agent
, referer
, domain
, remote
, data
, issue-time
, response
, data
, return-code
, content-type
, external-format
, headers
, cookies
, cookie
, name
, value
, domain
, path
, expires
, http-only
, secure
, cookie-header
, cookie
, get-var
, post-var
, post/get
, header
, file
, redirect
, serve-file
, request-run-time
, *debugger*
, handle-condition
, render-error-page
, execute-request
, set-data
, request
も参考にしてしてください。
1.3 ルーティング
リクエストがディスパッチされる前には、ルーティングシステムを通過します。他のフレームワークでは'routes'はどのハンドラがリクエストを処理するかを指定しますが、Radianceではその方式とは異なります。Radianceにおいてルート(route
)とは、URIを変換するフォームです。この部分は、2つの世界を作成して保持します。内部の世界と外部の世界です。
内部の世界は、実際にWebアプリケーションが住む世界です。外部の世界は、HTTPサーバとWebサイトを利用するユーザが住む世界です。この区別は、サーバの潜在的な罠を避けてWebアプリケーションを書くために必要不可欠です。あなたのアプリケーションを動作させるために、どのドメインやポート、パスが必要になるかを心配する必要がありません。一方で、アプリが壊れないように、Webの管理者として、システムをあなたの思う通りにカスタマイズして動作させる必要があります。
そのためには、ルーティングが役に立ちます。ルーティングは、Mapping
とReversal
の2種類があります。
Mapping
は、外部の世界からくるURIを内部の世界のURIに変換します。普通は、トップレベルのドメインを切り取り、サブドメインにマッピングします。
Reversal
は逆のことをします。内部の世界から外部の世界へといきます。このことは、あなたが提供するWebページが、実際に外部からアクセス可能なリソースを参照できるようにするために必要です。
ルーティングは、任意の処理を行うことができます。基本的なレベルでは、何らかの方法でURIを修正する関数です。ルーティングを使うと、強力で柔軟なシステムを構築することができます。アプリケーションの開発者として、external-uri
かuri-to-url
を、ページ内の全てのリンクに使うようにしてください。
route
, name
, direction
, priority
, translator
, route
, remove-route
, list-routes
, define-route
, define-matching-route
, define-target-route
, define-string-route
, internal-uri
, external-uri
も参考にしてください。
1.4 URIディスパッチャー
ついに、リクエストに対してコンテンツを生成する段階まできました。URIディスパッチャーはURIのサブクラスであり、名前、関数、優先順位を運びます。
優先順位に基づいたリストは、いつリクエストがきても実行されます。リクエストのURIは、それぞれのディスパッチャーに対応して、最初に対応する最初のディスパッチャー関数が実行されます。たったこれだけです。
ディスパッチャー関数は、ページの内容を提供するために、必要な値を、レスポンスオブジェクトに設定する責任があります。そのためには、レスポンスオブジェクトのdata
のフィールドに直接設定するか、関数から適切な値を返します。Radianceは、4つの型のデータ(stream
、pathname
、string
、(array (unsigned-byte 8))
)を受けとります。
もしURIディスパッチャーが明示的な優先順位の番号を持っていない場合、優先順位はURIの特異性によって決まります。どのように計算がされるかについて詳しく知りたい場合は、URIソーティング関数であるuri>
をみてください。
uri-dispatcher
, name
, dispatch-function
, priority
, uri-dispatcher
, remove-uri-dispatcher
, list-uri-dispatchers
, uri-dispatcher>
, define-uri-dispatcher
, dispatch
もご参照ください。
1.5 Page
ページは、実際にコンテンツを提供する関数を定義するために用いられます。しかし、ページはuriディスパッチャーであり、物事を簡単にするためのマクロをいくつか含んでいます。注目してもらいたいのは、拡張可能なオプションです。では、詳しくみていきましょう。
Radianceには、デフォルトでセットアップされるページがいくつかあります。favicon
とrobots
のページは、Radianceのstatic/
ディレクトリでサーブされます。本番環境のサーバでも、自分のサイトでファイルを提供したり更新したりしたいはずです。
そのような目的を満たすために、static
ページの仕組みがあります。static
ページは、静的コンテンツをWebアプリケーションとモジュールにサーブします。static
ページは、そのドメインでも/static/...
のパスで有効であり、最初のディレクトリがモジュールの名前である形式である必要があります。残りは、モジュールのstatic/
ディレクトリの範囲にあるパスです。この仕組みにより、CSS、JavaScript、画像などの静的ファイルを参照することができます。
最後に、api
ページですが、APIエンドポイントのディスパッチを処理する役割があります。これについては次の章で説明します。ページは、静的ファイルの場合と同様に、どのドメインにおいても/api/...
のパスで捕捉して動作します。
page
, remove-page
, define-page
をご参照ください。
1.6 APIエンドポイント
Radianceは、REST APIとの連携もサポートしています。これは、取ってつけたような機能ではありません。多くの現代のアプリケーションは、そのようなAPIを提供することが多く、Radianceは、APIエンドポイントを含むアプリケーションを書く方法を提案します。
コンセプトとしては、APIエンドポイントとは、ブラウザーのリクエストに応じて呼び出される関数です。そのレスポンスは、リクエスト主が読める形式にシリアライズされます。重要な点は、APIエンドポイントは、ユーザからも、プログラムからも利用可能であるべきということです。APIを通してプログラムから実行されるアクションは全て、ユーザによって実行されるものであるので、Radianceでは、両方から利用可能にするよう推奨しています。重複を避けるために、これら2つは、1つとして扱われます。
データの修正作業は全て、APIエンドポイントを通して提供されます。リクエストをしたのがユーザ
かプログラム
のどちらなのかを区別せずに処理されます。ユーザがリクエストを行う場合は、適切なページにリダイレクトを行います。プログラムからリクエストが行われた場合は、読み込めるフォーマットで、データのペイロードが提供されます。
全てのパートのうち、APIフォーマットのシステムですが、データを特別なフォーマットにシリアライズすることを担当します。デフォルトでは、S式を基本とするフォーマットが提供されていますが、JSON形式の出力も簡単にロードできます。
次に、browser
のPOST/GETパラメータの仕様をみましょう。そのパラメータが"true"
という文字列を含む場合、APIリクエストはユーザからであると扱われて、データのペイロードはされずにリダイレクトされます。
あなたのアプロケーションは、統一されたAPIを提供するために、これらをうまく使う必要があります。今、実際のエンドポイントの定義は、名前、関数、引数を記述するラムダリスト、リクエストのパーズ関数で構成されます。引数の典型としては、必須の引数かオプショナル引数が理にかなっています。結局、HTTPリクエストは、キーワード引数しかもつことができません。キーワード引数は、あってもなくてもいいです。
APIエンドポイントの名前は、どこにリーチできるかを示す識別名として機能します。APIエンドポイントは、/api/
パスに存在し、エンドポイントの名前もそれに従います。エンドポイントを修正する際には、あなたのモジュールやアプリケーションが、他のエンドポイントをうっかり踏み外さないように気をつけなければいけません。これはURIディスパッチのときとは違いますが、理由は、APIエンドポイントは、曖昧さやパスのプロセスを許可しないからです。なので、全てのエンドポイントは、唯一のパスをもなければいけません。唯一のパスは、直接、名前としてサーブします。
生の(raw)関数は、APIがインターフェイスを提供する関数です。生の関数は、リクエストされたアクションを行い、適切なデータを上で述べた通りに返す役割を果たします。フォーマットされたAPIのデータを返すためには、api-output
を使います。ブラウザリクエストからリクエストを受けてリダイレクトをするには、redirect
を使います。
最後に、リクエストのパーズ関数は、リクエストオブジェクトを受け取り、関数が必要な実引数を抽出して、最終的に、可能であれば、適切な引数を適用させて関数を呼び出します。パーズ関数は、もし必要な引数が見当たらない場合、api-argument-missing
エラーの信号を送ります。無駄な実引数は無視されます。
call-api
を用いると、プログラムからAPIエンドポイントを呼び出すこともできます。call-api-request
を使うと、Requestをシュミレーションすることができます。どちらもURIディスパッチの仕組みを通る必要はありません。
ページと似ていますが、APIエンドポイントの定義は、定義を簡単にするために、拡張オプションも受け付けます。オプションについては、次で説明します。
api
, *default-api-format*
, *serialize-fallback*
, api-format
, remove-api-format
, list-api-formats
, define-api-format
, api-output
, api-serialize
, api-endpoint
, remove-api-endpoint
, list-api-endpoints
, api-endpoint
, name
, handler
, argslist
, request-handler
, call-api-request
, call-api
, define-api
をご参照ください。
1.7 オプション
オプションは、拡張可能な定義のマクロを提供します。これは、フレームワークが何か定義する際に、共通のオペレーションをより短しようと拡張しようとするときに役に立ちます。例えば、アクセス権があるユーザに対して、ページかAPIエンドポイントを制限するような共通するタスクがあるとします。
このような実装を簡単にするために、Radianceは一般的なオプションに仕組みを提供します。オプションは、定義のマクロが属するオプションの型によって分けられます。Radianceは、api
とpage
のオプションを提供します。
それぞれのオプションは、オプションの型に応じて、名前と多くの引数を受け入れる関数のために、キーワードをもっています。定義名、ボディー部、最終にオプションに渡される値を含んだリストが、いつも引数として与えられます。
この拡張用(expansion)の関数は、定義マクロのボディーの式を変換します。環境の設定を許可するために、定義自体では含まれない式を吐き出すこともできます。
option
, option-type
, name
, expander
, option
, remove-option
, list-options
, define-option
, expand-options
をご参照ください。
1.8 モジュール
モジュールの概念は、Radianceにおいて必要不可欠です。全体を構成する部品として働きます。技術レベルでいうと、モジュールは、特別なメタデータが与えられたパッケージです。モジュールは、modularize
しステmyによって提供され、フック、トリガー、インターフェイス等を使いやすくして、他の情報をトラッキングするために使われます。
defpackage
を使う代わりに、define-module
を使うようにしてください。シンタックスはdefpackage
ですが、:domain
のような特別なオプションを含んでいるので、モジュールが機能するプライマリ・ドメインを特定することができます。
モジュールのシステムでは、ASDFのシステムをモジュールに記すこともできます。もし記せば、そのASDFシステムは仮想のモジュールになります。このようにするためには、システムの定義に3つのオプションを追加する必要があります:
:defsystem-depends-on (:radiance) :class "radiance:virtual-module" :module-name "MY-MODULE"
こうすることで、Radianceは、ASDFのシステム情報を特定して、あなたのモジュールに関連づけることができます。
新しいモジュールのために、必須のシステムとモジュールの定義を自動で行うには、create-module
をみてください。
virtual-module
, virtual-module-name
, define-module
, define-module-extension
, delete-module
, module
, module-p
, module-storage
, module-storage-remove
, module-identifier
, module-name
, current-module
, module-domain
, module-permissions
, module-dependencies
, module-required-interfaces
, module-required-systems
, module-pages
, module-api-endpoints
, describe-module
, find-modules-directory
, *modules-directory*
, create-module
をご参照ください。
1.9 フック
Radianceには、互いのモジュールを連携させるために、フック(hook)の仕組みがあります。フックを使うと、ある種のイベントに反応して、任意の関数を実行することができます。例えば、意見交換をするようなソフトウェアでは、新しい投稿が作成されるごとにトリガーされるフックを設定することがでいます。拡張では、追加のタスクを実行するためにトリガーをフックに対して定義できます。
フックは、任意の数のトリガーをもつことができますが、トリガーは長すぎないようにしてください。フックをトリガーすることは、全てのトリガーが終了するまでブロックされる動作だからです。そのようなわけで、長い間続くようなトリガーの実行は、リクエストへのレスポンスを遅らせてしまう可能性があります。 long.
フックは、長い間はonであり、後にoffになるスイッチにような関数であるべきです。もし新しいトリガーが実行中に定義された場合は、自動で呼び出されるべきです。これは、define-hook-switch
が容易にすることです。define-hook-switch
は2つのフックを作ります。1つ目がトリガーされると、それに定義されているトリガーは、後に2つ目のフックがトリガーされたときに、自動で呼び出されます。このおかげで、仮にトリガーがサーバの起動後に定義されたとしても、server-start
が適切に動作します。
list-hooks
, define-hook
, remove-hook
, define-trigger
, remove-trigger
, trigger
, define-hook-switch
をご参照ください。
1.10 インターフェイス
システムが一枚岩になることを避けるために、バックエンドは拡張できるようにするために、Radianceはインターフェイスの仕組みを含んでいます。一般的な意味では、インターフェイスとは、関数やマクロ、変数がどのように動作するかについて、約束事を決めますが、実際に実装するのはインターフェイスではありません。インターフェイスが動作する全てを作っている実際の機能性は、実装の外側にあります。このことにより、ユーザはインターフェイスに対してコードを書くことができ、特定のバックエンドに結びつけることなく、与えられた機能を利用することができます。
具体例として、データベースのためのインターフェイスをみていきましょう。データベースには多くの種類があり、全ては違う方法でやりとりをしますが、データの保存、検索、修正等、どれにも共通する操作があるので、このインターフェイスは実用的です。
では、共通する操作を提供するインターフェイスを作ります。これは、特定の種類のデータベースが実際に動かすためにすることは、実装次第です。アプリケーションの作者として、データベース・インターフェイスを活用することができます。このインターフェイスを使えば、様々なデータベースに対して自動的にうまく動作するようにできます。
これは、アプリケーション作者に有利なだけでなく、インターフェイスで分離することにより、システム管理者は、比較的容易に、実装に存在しない特殊な機能を自分で実装することができることです。インターフェイスが不透明であるおかげで、実装はLispのプロセスで動くものと、外部で動くプロセスとの橋渡しをすることができます。 これは、Productionシステムの管理者が必要な情報を選び出すために、開かれた選択肢を多く与えます。
実際、インターフェイスは特別な種類のモジュールであり、特別な種類のパッケージです。定義の一部として、関数や変数などの束縛のために、一連の定義を含みます。インターフェイスはパッケージなので、あなたがコンポーテントで使えるユーザは、他のパケージにあるものも何でも使えます。違いはありません。実装の作者として、あなたはインターフェイスが示す定義を再定義することができます。
実際にインターフェイスを利用するモジュールを読み込むためには、インターフェイスの実装は、事前に読み込まれる必要があります。そうでなければ、マクロは適切に動作しません。あなたのASDFシステムの定義において、特定の実装を参照する必要なく、インターフェイスに依存することを許可するためには、Radianceは拡張されたASDFを提供します。この拡張を使うと、(:interface :foo)
のようなリストを:depends-on
に追加することができます。モジュールがロードされたときに、Radianceはインターフェイスを具体的な処理に分解します。
Radianceは、標準のインターフェイスを提供します。それぞれのインターフェイスは、radiance-contribsによって提供される標準実装を1つ以上もちます。インターフェイスは次の通りです:
admin
拡張可能な管理者ページを提供します。auth
認証とログインに関する全てを扱います。ban
IPアドレスによってユーザがサイトにアクセスすることを禁止します。cache
キャッシュのための仕組みを提供します。database
柔軟なデータベースのインターフェイスです。オブジェクト保存、リレーショナルデータベースをバックエンドとして利用できます。logger
ログ出力のためのインターフェイスです。デバッグやメッセージを出力できます。mail
メールを送るための最小限のインターフェイスです。profile
ユーザのプロフィールや属性を拡張するために使います。rate
特定のリソースにrate limitationを許可します。server
Radianceを外部の世界と結びつける架け橋の役割を果たすインターフェイスです。session
トラッキングするために、持続するセッションを保証します。user
ユーザアカウントとパーミションの機能を提供します。
それぞれのインターフェイスについては、次の章で説明します。
interface
, interface-p
, implementation
, implements
, reset-interface
, define-interface-extension
, find-implementation
, load-implementation
, define-interface
, define-implement-trigger
をご参照ください。
1.11 環境
複数のRadianceインスタンスに対して、同じマシン内で異なる設定ができるように、Radianceでは環境(Environment)という仕組みを用意しています。環境とは、基本的には、Radianceとロードされるモジュールのための設定ファイルの一式です。Radianceの設定は、インターフェイスを選択される実装にマッピングすることで、もしインターフェイスが求められたときに選択されるように決定します。
startup
が呼び出された時、どれだけ遅くとも、特定の環境が選択されます。早ければ、モジュールがロードされたときに選択されます。後者の場合は、環境を選ぶために、インタラクティブな再起動が可能です。これは必須の機能ですが、理由は、そうでなければ、Radianceがインターフェイスのマッピングを解決できないからです。
環境のシステムの一部として、Radianceは、あなたのアプリケーションで使える(おそらく、使うべき)設定システムを提供します。設定システムを使うと、それぞれの環境ごとに、適切に設定することができます。その設定は、いつでも持続性があるうえ、可読性にも優れたフォーマットで保存されるので、特別なツールで読み込んだり、修正したりする必要はありません。
実際に設定手順でどのように保存処理がされているかを知りたい方は、ubiquitousを参考にしてください。 value
関数の代わりに、Radianceではconfig
関数を使えます。
environment-change
, environment
, check-environment
, mconfig-pathname
, mconfig-storage
, mconfig
, defaulted-mconfig
, config
, defaulted-config
をご参照ください。
1.12 インスタンスの管理
最後に、Radianceは、起動からシャットダウンまでのシーケンスを提供しています。このおかげで、ソフトは適切に起動して利用可能になり、その後、綺麗に片付けて終了することを確かにします。
そのシーケンスの大部分は、正しい順番で、適切な回数、特定のフックが呼び出されることによって実現されています。
インターフェイスの関数を適切に使うことで、サーバを手動で起動することは可能ですが、その方法で、アプリケーションが正しく動作すると想定するのはやめてください。多くは、特定のフックが適切な順番で呼び出されることを要求しています。このような理由で、startup
とshutdown
でRadianceインスタンスを管理する必要があります。startup
とshutdown
のドキュメントには、どのフックが、どの順番で呼び出されているかが説明されています。実装では、シンボルがexportされていない限り、追加で特定されない定義をインターフェオスのパッケージのシンボルに加えることができます。
*startup-time*
, uptime
, server-start
, server-ready
, server-stop
, server-shutdown
, startup
, startup-done
, shutdown
, shutdown-done
, started-p
をご参照ください。
2. 標準のインターフェイス
標準のインターフェイスは、Radianceとcore packageと一緒に配布されています。ライブラリは、追加のインターフェイスを提供することも可能です。インターフェイスの実装ですが、インターフェイス定義では、次の制限の緩和が許可されています:
&key
引数を含むラムダリストは、実装依存のキーワード引数を使って拡張できます。&optional
引数を含み、&key
か&rest
を含まないラムダリストは、オプショナル引数で拡張できます。必須の引数しか含まないラムダリストは、オプショナル引数かキーワード引数で拡張できます。
2.1 管理
管理・インターフェイスは、管理者ページを作成するためのものです。ユーザ構成の設定やシステム情報の表示のために使えます。"管理(administration)"という名前ですが、システムの管理者だけに限ったものではありません。どのユーザにも利用できることができます。
管理者ページは、カテゴリー分けされたメニュやパネルを表示できなければいけません。パネル群は、他のモジュールによって提供されており、admin:define-panel
で追加できます。秘密情報を含むようなパネルにアクセスするパネルは、:access
オプションでアクセスを禁止させるようにしてください。
パーミッションについては、 userインターフェイスを参照してください。
管理者ページや特定のパネルにリンクをはるには、page
リソースを使ってください。
admin:list-panels
, admin:remove-panel
, admin:define-panel
, admin:panel
をご覧ください。
2.2 認証
認証・インターフェイスは、ユーザをリクエストと結びつけます。そのために、ユーザがシステムに対して自分自身を認証させる方法を提供しなければいけません。どのように実現するかは、実装次第です。実装は、認証のプロセスが初期化するためのページを提供しなければいけません。page
のソースを通してURIを認証プロセスに渡して、"login"
を引数として渡します。
現在リクエストに結び付けられているユーザをauth:current
コマンドで調べることができます。ユーザが"anonymous"
と解釈される場合は、NIL
を返します。詳しくはuserインターフェイスを参照ください。
auth:*login-timeout*
, auth:page
, auth:current
, auth:associate
もご覧ください。
2.3 ban
ban・インターフェイスは、IPによるアクセス制限を提供します。IP BANされたクライアントのIPアドレスからは、リクエストするページに対してアクセス出来なくなります。BANは、タイムアウトの後、手動・自動いずれでも、離す(lift)することができます。実装としては、ユーザのIPを監視するために、追加で労力を割くことは想定していません。
ban:jail
, ban:list
, ban:jail-time
, ban:release
をご参照ください。
2.4 キャッシュ
キャッシュ・インターフェイスは、一般的なキャッシュの仕組みを提供します。カスタマイズ可能で無効化のテストをもっています。キャッシュを明示的に新しくするには、cache:renew
をつかいます。cache:with-cache
を使うと、キャッシュの部品を構築することができ、テストのformがtrueと評価されたときに、BODYのキャッシュ値が返されるようにできます。
キャッシュ値が保存される方法は実装によります。 cache:get
かcache:with-cache
を使うと、キャッシュ値は文字列かバイト列に強制変換されます。実装では、キャッシュに格納できる typeには制限がありませんが、少なくとも、文字列とバイト列はサポートします。
キャッシュ値の名前は、名前とパッケージ名が、次の文字を含まないシンボルでなければいけません:<>:"/\|?*.
キャッシュ値の変形は、princ
によって表示される表現とは区別されるオブジェクトである必要があります。その際には、先ほどと同じ文字の制限が適用されます。
cache:get
, cache:renew
, cache:with-cache
をご参照ください。
2.5 データベース
データベース・インターフェイスは、データを持続させるためのレイヤーを提供します。通常は、データベースと呼ばれるレイヤーです。リレーショナル型のデータベースである必要はありませんが、そうであってもいいです。実装の変数を保持するために、基本的なデータベースの機能しかサポートされません。(joinsやtriggers等はありません)データ型も、整数、float、文字列に限定されます。これらの制限にも関わらず、多くのアプリケーションにおいて、データベースインターフェイスはとても役に立ちます。
伝統的なRDMBの用語と区別するために、特別な用語が使われます:
schema
は"structure"、table
は"collection"、row
は"record"とします。
データベースに接続する前に、データベース関連の命令を実行すると、未定義の動作を招きます。Radianceでは、Radianceが動作している間はデータベースが接続されることを保障しているので、どのページ、どのAPI、どのURIディスパッチャーの定義において、データベースインターフェイスを問題なく使えます。
実際にデータ保存を行うための関数は、db:insert
、db:remove
、db:update
、db:select
、db:iterate
です。
それらは、あなたが期待するように動作するはずです。
詳しくは、それぞれの関数に書かれているコメントを読んでください。コレクションの作り方、どのような制限があるかを知りたい方は、db:create
のコメントも参考にしてください。
データベースは、データ操作が完了すれば、Radianceを再起動したり、Lispイメージ、マシンがクラッシュしたとしても、データの変更は存続されなければいけません。
database:condition
, database:connection-failed
, database:connection-already-open
, database:collection-condition
, database:invalid-collection
, database:collection-already-exists
, database:invalid-field
, database:id
, database:ensure-id
, database:connect
, database:disconnect
, database:connected-p
, database:collections
, database:collection-exists-p
, database:create
, database:structure
, database:empty
, database:drop
, database:iterate
, database:select
, database:count
, database:insert
, database:remove
, database:update
, database:with-transaction
, database:query
, database:connected
, database:disconnected
を参照してください。
2.6 logger
logger・インターフェースは、ログの関数を提供します。システムの中で関連して起きていることについて、ログのメッセージを出すことができます。ログの出力内容と出力方法に関しては、実装とシステムの管理者次第です。
logger:log
, logger:trace
, logger:debug
, logger:info
, logger:warn
, logger:error
, logger:severe
, logger:fatal
をご覧ください。
2.7 メール
メール・インターフェースは、メールを送る仕組みを組み込むことができます。様々なコンポーネントが、Webサイトの外からユーザとつながるために、メールのアクセスが必要になるかもしれません。リモートサーバ、ローカル環境でのメール送信等、メールの送信方法の設定は、処理系依存です。mail:send
のフックを使うと、メールが送られる前に、メールに反応することができます。
mail:send
をご覧ください。
2.8 プロフィール
プロフィール・インターフェイスは、userにある種のpresenceをもたせたいアプリケーションにおいて、共通で使用されるuserインターフェイスを拡張できるようにします。そのインターフェイスは、機能の一部として、ユーザのプロフィールが表示されるページを提供する必要があります。そのプロフィールは、数種類のパネルを表示しなければいけません。パネルは、他のモジュールによって提供されており、profile:define-panel
で追加できます。
page
のリソースの型を通して、URIをプロファイルのページに移動することができます。
そのインターフェイスは、視覚的にユーザを特定させるためにprofile:avatar
でアバター画像にアクセスさせることもできます。また、profile:name
を使うと、ユーザがユーザ名をカスタマイズできます。さらに、profile:fields
、profile:add-field
、profile:remove-field
を使うと、どのようなデータをユーザの属性に含むか、それを公(public)に表示させるかどうかを指定できます。
profile:page
, profile:avatar
, profile:name
, profile:fields
, profile:add-field
, profile:remove-field
, profile:list-panels
, profile:remove-panel
, profile:define-panel
, profile:panel
をご参照ください。
2.9 rate
rate・インターフェースは、Rate limitationの仕組みを提供します。秘密情報やコストが高いリソースへの負荷の高いアクセスを防ぐことができます。2つの段階があります。第1段階は、rate:define-limit
により、特定のリソースに対して、Rate limitationの動作を定義します。第1段階は、リソースがrate:with-limitation
により保護されます。
もし、特定のユーザからのblockへのアクセスが頻繁すぎる場合は、blockは呼び出されません。制限の定義があるコードが、代わりに実行されます。
Rate limitation
は、クライアント、ユーザ、セッションごとですが、グローバルではないことに注意してください。
rate:define-limit
, rate:left
, rate:with-limitation
をご参照ください。
2.10 サーバ
serverインターフェイスとloggerインターフェイスは、Radianceが起動時に順番通りに読み込まれます。HTTPリクエストに応答する責任があります。実装では、リクエストを受け入れて、Radianceのrequest
関数に渡す必要があります。その後、response
はリクエスト主に戻されます。
リスナーの動作を特定する引数は実装によることに注意してください。しかし、実装は、(mconfig :radiance :port)
で設定されたlocalhost
とポートからでアクセスできる標準のリスナーを提供して、radiance:startup
で起動できるようにする必要があります。
server:start
, server:stop
, server:listeners
, server:started
, server:stopped
をご参照ください。
2.11 セッション
セッション・インターフェースは、あるクライアントが行う複数のリクエストを追跡します。クライアントによっては情報を隠蔽したり偽装している場合があるので、完全にはクライアントを追跡できるとはいえません。しかし、多くのユーザに対しては、うまく動作するはずです。セッション・インターフェイスは、他のインターフェイスや低レイヤーのライブラリの中で使われます。ユーザ認証にような一貫性を保つために使われます。
session:*default-timeout*
, session:session
, session:=
, session:start
, session:get
, session:list
, session:id
, session:field
, session:timeout
, session:end
, session:active-p
, session:create
をご参照ください。
2.12 ユーザ
ユーザ・インターフェースは、ユーザオブジェクトを永続させ、パーミションの仕組みを組み込めます。ユーザ認証、ユーザの特定、トラッキング等は扱いません。このインターフェイスでは、ユーザオブジェクトを提供するのみであり、パーミション情報が管理されます。
パーミションに関する詳細は、user:user
を参照してください。
user:condition
, user:not-found
, user:user
, user:=
, user:list
, user:get
, user:username
, user:fields
, user:field
, user:remove-field
, user:remove
, user:check
, user:grant
, user:revoke
, user:add-default-permissions
, user:create
, user:remove
, user:action
, user:ready
, user:unready
もご覧ください。
参考
- modularize パッケージのメタシステム
- modularize-interfaces インターフェイスと実装の拡張
- modularize-hooks フックとトリガーの仕組み
- ubiquitous 環境設定
- radiance-contribs インターフェイスの実装、他便利な機能
Common Lisp – destructuring-bind
元の記事はこちらです:
Common Lisp – destructuring-bind / Kyle Burton著
Common Lisp – destructuring-bind
リストの分解
;; これが定番の使い方です。リストを分解します。 (destructuring-bind (first second) '(1 2) (format t "first:~A second:~A ~&" first second)) ;;; => first:1 second:2
ドット表記
;; ドット表記のリストも分解できます。 (destructuring-bind (first . second) '(1 . 2) (format t "first:~A second:~A ~&" first second)) ;;; => first:1 second:2
ドット表記で残りの引数を全て捕捉
;; destructuring-bindの第一引数はラムダリストですが、 ;; ドット記法を使うと、残り全ての引数を捕捉することができます。 (destructuring-bind (first second . stuff) '(1 2 3 4 5) (format t "first:~A second:~A rest:~A ~&" first second stuff)) ;;; => first:1 second:2 rest:(3 4 5)
&restで残りの引数を全て捕捉
;; また、レスト引数を使って残り全ての引数を捕捉することもできます。 (destructuring-bind (first second &rest stuff) '(1 2 3 4 5) (format t "first:~A second:~A rest:~A ~&" first second stuff)) ;;; => first:1 second:2 rest:(3 4 5)
&optionalでデフォルト値を設定
;; オプショナル引数を使って、引数にデフォルト値を設定することもできます。 (destructuring-bind (first second &optional (third 'default)) '(1 2) (format t "first:~A second:~A third:~A ~&" first second third)) ;;; => first:1 second:2 third:DEFAULT (destructuring-bind (first second &optional (third 'default)) '(1 2 3) (format t "first:~A second:~A third:~A ~&" first second third)) ;;; => first:1 second:2 third:3
キーワード引数の利用
;; また、キーワード引数を使うこともできます。 (destructuring-bind (first second &key third) '(1 2 :third 3) (format t "first:~A second:~A third:~A ~&" first second third)) ;;; => first:1 second:2 third:3
木構造を逆パース
;; 最後に、木構造を逆パース(unparse)するためにも使うことができます。 ;; 分解したいデータ構造に対して、あなた自身の変数宣言を適用することができます。 ;; このテクニックは、XMLをS式に変換した後、データを処理する際に役立ちます。 (destructuring-bind (a (b (c d e (f g) h i j)) &rest remainder) '(1 (2 (3 4 5 (6 7) 8 9 10)) 11 12 13 14 15) (format t "a:~A b:~A c:~A d:~A e:~A f:~A g:~A h:~A i:~A j:~A remainder:~A ~&" a b c d e f g h i j remainder)) ;;; => a:1 b:2 c:3 d:4 e:5 f:6 g:7 h:8 i:9 j:10 remainder:(11 12 13 14 15)
Caveman2 - README 訳
Caveman(2018年8月14日時点)のREADMEの和訳です。READMEの更新に合わせて、こちらの和訳も更新します。
Caveman2 - 軽量なWebアプリケーションフレームワーク
利用方法
(defparameter *web* (make-instance '<app>)) @route GET "/" (defun index () (render #P"index.tmpl")) @route GET "/hello" (defun say-hello (&key (|name| "Guest")) (format nil "Hello, ~A" |name|))
Caveman2とは?
Caveman1との相違点
Caveman2は、ゼロから書き直しました。
重要な点は、次の通りです:
ゼロから書き直した理由
「ningleとCaveman、どちらを使うべきですか?」
「違いは何ですか?」
と聞かれることがよくありました。
両者の役割が似ていることが原因だったと思います。
どちらも小さなフレームワークであり、データベースをサポートしています。
Caveman2は、小さなWebフレームワークではありません。CL-DBIをサポートし、デフォルトでデータベース接続を管理します。
デザインの目標
Cavemanは、Webアプリケーションの開発で共通する部品を集めたコレクションです。
Cavemanは、3つのルールのもと、開発されました。
- 拡張できること
- 実用的であること
- 何も強要しないこと
はじめに
あなたがここにきたということは、Caveman(洞窟男)のように暮らすことに、興味があるのでしょうか?
洞窟にはディズニーランドはありませんが、始めるには良い場所です。
さて、洞窟に入りましょう。
インストール
現在、Caveman2はQuicklispで入手できます。
(ql:quickload :caveman2)
プロジェクトのテンプレートを生成する
(caveman2:make-project #P"/path/to/myapp/" :author "<Your full name>") ;-> writing /path/to/myapp/.gitignore ; writing /path/to/myapp/README.markdown ; writing /path/to/myapp/app.lisp ; writing /path/to/myapp/db/schema.sql ; writing /path/to/myapp/shlyfile.lisp ; writing /path/to/myapp/myapp-test.asd ; writing /path/to/myapp/myapp.asd ; writing /path/to/myapp/src/config.lisp ; writing /path/to/myapp/src/db.lisp ; writing /path/to/myapp/src/main.lisp ; writing /path/to/myapp/src/view.lisp ; writing /path/to/myapp/src/web.lisp ; writing /path/to/myapp/static/css/main.css ; writing /path/to/myapp/t/myapp.lisp ; writing /path/to/myapp/templates/_errors/404.html ; writing /path/to/myapp/templates/index.tmpl ; writing /path/to/myapp/templates/layout/default.tmpl
ルーティング
Caveman2は、ルーティングを定義するために、2通りの方法を提供しています。
@route
かdefroute
、どちらを使うかは、あなた次第です。
@route
は、アノテーションのマクロであり、cl-annotを用いています。
「メソッド」 「URL文字列」 「関数」 を受け取ります。
@route GET "/" (defun index () ...) ;; 無名のroute @route GET "/welcome" (lambda (&key (|name| "Guest")) (format nil "Welcome, ~A" |name|))
@route
は、引数のリスト以外は、Caveman1の@url
と似ています。
必要がない場合は、引数を指定する必要はありません。
defroute
は単なるマクロであり、@route
と同じ機能を提供します。
(defroute index "/" () ...) ;; 無名のroute (defroute "/welcome" (&key (|name| "Guest")) (format nil "Welcome, ~A" |name|))
Cavemanは、ningleを元に作られているので、Sinatraのようなルーティングも使えます。
;; GET request (default) @route GET "/" (lambda () ...) (defroute ("/" :method :GET) () ...) ;; POST request @route POST "/" (lambda () ...) (defroute ("/" :method :POST) () ...) ;; PUT request @route PUT "/" (lambda () ...) (defroute ("/" :method :PUT) () ...) ;; DELETE request @route DELETE "/" (lambda () ...) (defroute ("/" :method :DELETE) () ...) ;; OPTIONS request @route OPTIONS "/" (lambda () ...) (defroute ("/" :method :OPTIONS) () ...) ;; For all methods @route ANY "/" (lambda () ...) (defroute ("/" :method :ANY) () ...)
ルートパターンには、値を引数に入れるために、キーワードを含むことができます。
(defroute "/hello/:name" (&key name) (format nil "Hello, ~A" name))
上のコントローラーでは、 "/hello/Eitaro"にアクセスがきたときname
は"Eitaro"になり、"/hello/Tomohiro"にアクセスがきたときname
は"Tomohiro"になります。
(&key name)
はCommon Lispのラムダリストとほぼ同じですが、他のkeyを含むこともできます。
(defroute "/hello/:name" (&rest params &key name) ;; ... )
ルートパターンには、ワイルドカード引数を含めることもできます。splat
でアクセス可能です。
(defroute "/say/*/to/*" (&key splat) ; /say/hello/to/world にマッチします splat ;=> ("hello" "world") )) (defroute "/download/*.*" (&key splat) ; /download/path/to/file.xml にマッチします splat ;=> ("path/to/file" "xml") ))
URLのルールに正規表現を使うときには、:regexp t
を明記してください。
(defroute ("/hello/([\\w]+)" :regexp t) (&key captures) (format nil "Hello, ~A!" (first captures)))
通常、ルート(routes)は、定義された順番通りにマッチします。
はじめにマッチしたルートが呼びだされ、残りは無視されます。
next-route
を使うことで、次にマッチするルートに処理を渡すことができます。
(defroute "/guess/:who" (&key who) (if (string= who "Eitaro") "You got me!" (next-route))) (defroute "/guess/*" () "You missed!")
defroute
の結果として、次のフォーマットを返すことができます。
- 文字列
- パス名
- Clackのレスポンスのリスト(Status、Headers、Bodyを含みます)
クエリー問い合わせ/POSTパラメータ
角括弧"[" & "]"
を含むパラメータのキーは、クエリー問い合わせとしてパースされます。
ルータ(routers)で_parsed
としてパースされたパラメータにアクセスすることができます。
<form action="/edit"> <input type="name" name="person[name]" /> <input type="name" name="person[email]" /> <input type="name" name="person[birth][year]" /> <input type="name" name="person[birth][month]" /> <input type="name" name="person[birth][day]" /> </form>
(defroute "/edit" (&key _parsed) (format nil "~S" (cdr (assoc "person" _parsed :test #'string=)))) ;=> "((\"name\" . \"Eitaro\") (\"email\" . \"e.arrows@gmail.com\") (\"birth\" . ((\"year\" . 2000) (\"month\" . 1) (\"day\" . 1))))" ;=> "((\"name\" . \"Eitaro\") (\"email\" . \"e.arrows@gmail.com\") (\"birth\" . ((\"year\" . 2000) (\"month\" . 1) (\"day\" . 1))))"
空白のキーは、多値を含むことを表しています。
<form action="/add"> <input type="text" name="items[][name]" /> <input type="text" name="items[][price]" /> <input type="text" name="items[][name]" /> <input type="text" name="items[][price]" /> <input type="submit" value="Add" /> </form>
(defroute "/add" (&key _parsed) (format nil "~S" (assoc "items" _parsed :test #'string=))) ;=> "(((\"name\" . \"WiiU\") (\"price\" . \"30000\")) ((\"name\" . \"PS4\") (\"price\" . \"69000\")))"
テンプレート
Cavemanは、デフォルトのテンプレートエンジンとして、Djulaを採用しています。
{% extends "layouts/default.html" %} {% block title %}Users | MyApp{% endblock %} {% block content %} <div id="main"> <ul> {% for user in users %} <li><a href="{{ user.url }}">{{ user.name }}</a></li> {% endfor %} </ul> </div> {% endblock %}
(import 'myapp.view:render) (render #P"users.html" '(:users ((:url "/id/1" :name "nitro_idiot") (:url "/id/2" :name "meymao")) :has-next-page T))
Djulaを用いて、データベースから何か取得したり、関数を実行するには、どうしたらいいでしょうか。
レンダー(render)に、実行された引数を渡すときには、明示的にlist関数
を使う必要があります。
(import 'myapp.view:render) (render #P"users.html" (list :users (get-users-from-db)))
JSON API
次は、JSON APIの使用例です。
(defroute "/user.json" (&key |id|) (let ((person (find-person-from-db |id|))) ;; person => (:|name| "Eitaro Fukamachi" :|email| "e.arrows@gmail.com") (render-json person))) ;=> {"name":"Eitaro Fukamachi","email":"e.arrows@gmail.com"}
render-json
は、スケルトンプロジェクトの一部です。 コードは、"src/view.lisp"にあります。
静的なファイル
"static/"フォルダーにある画像、css、js、favicon.ico、robot.txtは、デフォルトでサーブされます。
/images/logo.png => {PROJECT_ROOT}/static/images/logo.png /css/main.css => {PROJECT_ROOT}/static/css/main.css /js/app/index.js => {PROJECT_ROOT}/static/js/app/index.js /robot.txt => {PROJECT_ROOT}/static/robot.txt /favicon.ico => {PROJECT_ROOT}/static/favicon.ico
"PROJECT_ROOT/app.lisp"を書き直すことで、このルールを変更することができます。
詳細は、Clack.Middleware.Staticを参照してください。
環境設定
Cavemanは、環境設定を切り替えるために、Envyを採用しています。
Envyを使うと、複数の環境を定義して、環境設定を環境変数で切り替えることができます。
次は、典型的な利用方法です。
(defpackage :myapp.config (:use :cl :envy)) (in-package :myapp.config) (setf (config-env-var) "APP_ENV") (defconfig :common `(:application-root ,(asdf:component-pathname (asdf:find-system :myapp)))) (defconfig |development| '(:debug T :databases ((:maindb :sqlite3 :database-name ,(merge-pathnames #P"test.db" *application-root*))))) (defconfig |production| '(:databases ((:maindb :mysql :database-name "myapp" :username "whoami" :password "1234") (:workerdb :mysql :database-name "jobs" :username "whoami" :password "1234")))) (defconfig |staging| `(:debug T ,@|production|))
全ての環境設定は、plistです。APP_ENV
を設定することで、どの開発環境を使うかを選ぶことができます。
現在の環境設定から値を得るには、myapp.config:config
に入手したいキーと一緒に呼び出します。
(import 'myapp.config:config) (setf (osicat:environment-variable "APP_ENV") "development") (config :debug) ;=> T
データベース
:databases
を環境設定に加えると、Cavemanはデータベースのサポートを有効化します。
:databases
は、データベースの設定を含んだalistです。
(defconfig |production| '(:databases ((:maindb :mysql :database-name "myapp" :username "whoami" :password "1234") (:workerdb :mysql :database-name "jobs" :username "whoami" :password "1234"))))
myapp.db
にあるdb
は、上で設定された各々のデータベースに接続するための関数です。
次のように使えます。
(use-package '(:myapp.db :sxql :datafly)) (defun search-adults () (with-connection (db) (retrieve-all (select :* (from :person) (where (:>= :age 20))))))
接続は、Lispセッションの間は有効であり、それぞれのHTTPリクエストで再利用されます。
retrieve-all
とクエリ言語は、dataflyとSxQLからきています。
詳細は、それぞれのドキュメントをご参照ください。
HTTPヘッダー/HTTPステータスを設定する
HTTPリクエストの間、特別な変数を利用できます。
*request*
と*response*
は、リクエストとレスポンスを表します。
Clackを使い慣れている場合は、Clack.Requestと Clack.Responseのサブクラスのインスタンスもあります。
(use-package :caveman2) ;; Get a value of Referer header. (http-referer *request*) ;; Set Content-Type header. (setf (getf (response-headers *response* :content-type) "application/json") ;; Set HTTP status. (setf (status *response*) 304)
全ての"*.json"へのリクエストに対して、Content-Typeとして"application/json"を設定するには、next-route
が便利です。
(defroute "/*.json" () (setf (getf (response-headers *response*) :content-type) "application/json") (next-route)) (defroute "/user.json" () ...) (defroute "/search.json" () ...) (defroute ("/new.json" :method :POST) () ...)
Using session
セッションデータとは、ユーザー固有のデータを記憶するためのものです。
*session*
は、セッションデータを表すハッシュテーブルです。
次の例では、セッションでの:counter
を増やして、それぞれの訪問者に表示しています。
(defroute "/counter" () (format nil "You came here ~A times." (incf (gethash :counter *session* 0))))
Caveman2は、デフォルトで、セッションデータをメモリに保存します。
変更するには、"PROJECT_ROOT/app.lisp"にある:store
を:session
に指定します。
To change it, specify :store
to :session
in .
次の例では、保存するためにRDBMSを使っています。
'(:backtrace :output (getf (config) :error-log)) nil) - :session + (:session + :store (make-dbi-store :connector (lambda () + (apply #'dbi:connect + (myapp.db:connection-settings))))) (if (productionp) nil (lambda (app)
注: あなたのアプリの:depends-on
として、:lack-session-store-dbi
を追加するのを忘れないように気をつけてください。
それはClack/Lackの一部ではありません。
詳しい情報は、Lack.Session.Store.DBi
のコードをご覧ください。
HTTPステータスコードを投げる
(import 'caveman2:throw-code) (defroute ("/auth" :method :POST) (&key |name| |password|) (unless (authorize |name| |password|) (throw-code 403)))
エラーページを特定する
404, 500等のエラーページを特定するには、アプリ内でon-exception
メソッドを定義してください。
(defmethod on-exception ((app <web>) (code (eql 404))) (declare (ignore app code)) (merge-pathnames #P"_errors/404.html" *template-directory*))
サーバを起動する
あなたのアプリは、起動と停止のために、start
とstop
という名前の関数をもっています。
あなたのアプリが"myapp"という名前だとすると、次のようになります。
(myapp:start :port 8080)
Cavemanは、Clack/Lackを元にしているので、Hunchentoot、mod_lisp、FastCGIのどのサーバを使うかを選択することができます。
(myapp:start :server :hunchentoot :port 8080) (myapp:start :server :fcgi :port 8080)
ローカル環境ではHunchentoot、本番環境ではFastCGIかWooを使うことをおすすめします。
clackupコマンドを使って、アプリを起動することもできます。 clackup command.
$ ros install clack
$ which clackup
/Users/nitro_idiot/.roswell/bin/clackup
$ APP_ENV=development clackup --server :fcgi --port 8080 app.lisp
ホットデプロイ
Cavemanにはホットデプロイの機能はありませんが、PerlモジュールのServer::Starterを使うと簡単にできます。
$ APP_ENV=production start_server --port 8080 -- clackup --server :fcgi app.lisp
注: Server::Starter
は、サーバが特定のFDにバインドできる必要があるので、start_server
コマンドで動作するのは、:fcgi
と:woo
だけです。
サーバを再起動するには、HUPシグナル(kill -HUP <pid>
)を、start_server
プロセスに送ってください。
エラーログ
Cavemanは、エラーのバックトレースを、:error-log
で特定したファイルに書き出します。
(defconfig |default| `(:error-log #P"/var/log/apps/myapp_error.log" :databases ((:maindb :sqlite3 :database-name ,(merge-pathnames #P"myapp.db" *application-root*)))))
他のテンプレートエンジンを使う
CL-WHO
(import 'cl-who:with-html-output-to-string) (defroute "/" () (with-html-output-to-string (output nil :prologue t) (:html (:head (:title "Welcome to Caveman!")) (:body "Blah blah blah.")))) ;=> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\"> ; <html><head><title>Welcome to Caveman!</title></head><body>Blah blah blah.</body></html>"
CL-Markup
(import 'cl-markup:xhtml) (defroute "/" () (xhtml (:head (:title "Welcome to Caveman!")) (:body "Blah blah blah."))) ;=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"><html><head><title>Welcome to Caveman!</title></head><body>Blah blah blah.</body></html>"
cl-closure-template
{namespace myapp.view} {template renderIndex} <!DOCTYPE html> <html> <head> <title>"Welcome to Caveman!</title> </head> <body> Blah blah blah. </body> </html> {/template}
(import 'myapp.config:*template-directory*) (closure-template:compile-cl-templates (merge-pathnames #P"index.tmpl" *template-directory*)) (defroute "/" () (myapp.view:render-index))
(ql:quickload :clack-middleware-clsql) (import 'clack.middleware.clsql:<clack-middleware-clsql>) (builder (<clack-middleware-clsql> :database-type :mysql :connection-spec '("localhost" "db" "fukamachi" "password")) *web*)* [Clack.Middleware.Clsql](http://quickdocs.org/clack/api#system-clack-middleware-clsql) * [CLSQL: Common Lisp SQL Interface](http://clsql.b9.com/) ### Postmodern ClackアプリでPostmodernを使うためには、`Clack.Middleware.Postmodern`を使ってください。 Cavemanでは、"PROJECT_ROOT/app.lisp"の`builder`にミドルウェアを追加してください。
(ql:quickload :clack-middleware-postmodern) (import 'clack.middleware.postmodern:<clack-middleware-postmodern>) (builder (<clack-middleware-postmodern> :database "database-name" :user "database-user" :password "database-password" :host "remote-address") *web*)* [Clack.Middleware.Postmodern](http://quickdocs.org/clack/api#system-clack-middleware-postmodern) * [Postmodern](http://marijnhaverbeke.nl/postmodern/) -->
参考
- Clack - Web application environment.
- Lack - The core of Clack.
- ningle - Super micro web application framework Caveman bases on.
- Djula - HTML Templating engine.
- CL-DBI - Database independent interface library.
- SxQL - SQL builder library.
- Envy - Configuration switcher.
- Roswell - Common Lisp implementation manager.
作者
- Eitaro Fukamachi (e.arrows@gmail.com)
ライセンス
Licensed under the LLGPL License.
ningle - README 訳
ningle(2018年8月14日時点)のREADMEの和訳です。READMEの更新に合わせて、こちらの和訳も更新します。
ningle
ningleは、Common Lisp製の軽量Webアプリケーションフレームワークです。
使い方
(defvar *app* (make-instance 'ningle:<app>)) (setf (ningle:route *app* "/") "Welcome to ningle!") (setf (ningle:route *app* "/login" :method :POST) #'(lambda (params) (if (authorize (cdr (assoc "username" params :test #'string=)) (cdr (assoc "password" params :test #'string=))) "Authorized!" "Failed...Try again."))) (clack:clackup *app*)
ここまで進めてブラウザで http://localhost:5000/ にアクセスすると、ningleが"Welcome to ningle!"と表示してくれます。
インストール
(ql:quickload :ningle)
ningleについて
ningleは、Cavemanのフォークプロジェクトです。Cavemanではプロジェクトの雛形を生成しますが、ningleでは生成しません。
ningleは軽量のフレームワークなので、Clackについて少し知識が必要です。Clackは、ningleの元になっているサーバ・インターフェイスです。
はじめに
ルーティング
ningleはSinatraのようなルーティングシステムをもっています。
;; GET request (デフォルト) (setf (ningle:route *app* "/" :method :GET) ...) ;; POST request (setf (ningle:route *app* "/" :method :POST) ...) ;; PUT request (setf (ningle:route *app* "/" :method :PUT) ...) ;; DELETE request (setf (ningle:route *app* "/" :method :DELETE) ...) ;; OPTIONS request (setf (ningle:route *app* "/" :method :OPTIONS) ...)
ルーティングのパターンには、引数に値を設定するために、キーワードを含むことができます。
(setf (ningle:route *app* "/hello/:name") #'(lambda (params) (format nil "Hello, ~A" (cdr (assoc :name params)))))
上のコントローラでは、"/hello/Eitaro"や"/hello/Tomohiro"にアクセスしたときに呼び出されます。(cdr (assoc :name params))`は、"Eitaro"と"Tomohiro"になります。
ワイルドカードを含めることも可能です。(assoc :splat params)
でアクセスできます。
(setf (ningle:route *app* "/say/*/to/*") #'(lambda (params) ; matches /say/hello/to/world (cdr (assoc :splat params)) ;=> ("hello" "world") )) (setf (ningle:route *app* "/download/*.*") #'(lambda (params) ; matches /download/path/to/file.xml (cdr (assoc :splat params)) ;=> ("path/to/file" "xml") ))
正規表現を使うことも可能です:
(setf (ningle:route *app* "/hello/([\\w]+)" :regexp t) #'(lambda (params) (format nil "Hello, ~A!" (first (cdr (assoc :captures params))))))
必要条件
Routeは、様々な整合条件を含むことができます。例えば、Acceptの場合は次のようになります:
(setf (ningle:route *app* "/" :accept '("text/html" "text/xml")) #'(lambda (params) (declare (ignore params)) "<html><body>Hello, World!</body></html>")) (setf (ningle:route *app* "/" :accept "text/plain") #'(lambda (params) (declare (ignore params)) "Hello, World!"))
独自に条件を定義することも簡単にできます。
(setf (ningle:requirement *app* :probability) #'(lambda (value) (<= (random 100) value))) (setf (ningle:route *app* "/win_a_car" :probability 10) #'(lambda (params) (declare (ignore params)) "You won!")) (setf (ningle:route *app* "/win_a_car") #'(lambda (params) (declare (ignore params)) "Sorry, you lost."))
リクエストとレスポンス
ningleには特別な変数が2つあります。*request*
と*response*
です。これらは、毎回のリクエストの度に、Lack.RequestとLack.Responseに束縛されます。
例えば、これらを使うことで、それぞれのコントローラにおいて、レスポンスのステータスコードやContent-Type等を変更できます。
(setf (lack.response:response-headers *response*) (append (lack.response:response-headers *response*) (list :content-type "application/json"))) (setf (lack.response:response-headers *response*) (append (lack.response:response-headers *response*) (list :access-control-allow-origin "*"))) (setf (lack.response:response-status *response*) 201)
Context
ningleには、context
という便利な機能があります。context
は、内部のハッシュテーブルにアクセスするために使います。
(setf (context :database) (dbi:connect :mysql :database-name "test-db" :username "nobody" :password "nobody")) (context :database) ;;=> #<DBD.MYSQL:<DBD-MYSQL-CONNECTION> #x3020013D1C6D>
セッションを利用する
ningle自体にセッションの仕組みはありませんが、Lack.Builderと一緒にLack.Middleware.Sessionを使うことをおすすめします。
(import 'lack.builder:builder) (clack:clackup (builder :session *app*))
もちろん、ningleとあわせて、他のLackミドルウェアを使うこともできます。
参考
作者
- Eitaro Fukamachi (e.arrows@gmail.com)
著作権
Copyright (c) 2012-2014 Eitaro Fukamachi (e.arrows@gmail.com)
ライセンス
Licensed under the LLGPL License.