目次


IBM Cloud 上で Clojure Web アプリを作成する

IBM Cloud インフラストラクチャーに支えられた関数型プロラミングの詳細を探る

Comments

関数型プログラミングの世界に足を踏み入れる心の準備はできていますか?関数型プログラミングを試そうと決心するわけは、Paul Graham 氏の Lisp に関する有名な小論文を読んだからかもしれません。物事に取り組む新しい方法を学びたいからかもしれません。あるいは、作成するコードやデータをどの程度まで抽象化できるか確かめてみたいからでしょうか。

理想的には、Web アプリケーションには IBM Cloud サーバー上で実行することで得られるパフォーマンスと高可用性が必要です。この理想を現実にするために、このチュートリアルでは Node.js を使用して Clojure プログラムを IBM Cloud アプリケーションの一部として実行する方法を説明します。

Clojure アプリケーションを作成するために必要なもの

  • IBM Cloudライト・アカウント (無料のIBM Cloudライト・アカウントを登録するか、アカウントを登録済みの場合は、そのアカウントで IBM Cloud にログインします)。
  • HTML および JavaScript の実用的知識
  • MEAN アプリケーション・スタック (少なくとも Node.js と Express) の実用的知識。MEAN アプリケーション・スタックに馴染みがない場合は、私が書いた、このリンク先の developerWorks シリーズで MEAN スタックの基礎を学ぶことができます。
  • Node.js アプリケーションを IBM Cloud にアップロードできる開発環境 (Eclipse など)

    アプリが IBM Cloud 内で実行中になったら、すぐに Git リポジトリーと継続的デリバリー・パイプラインを追加して、開発、テスト、デプロイを自動化することができます。そのようにセットアップするには、アプリの「Overview (概要)」ページで「Add Git Repo And Pipeline (Git リポジトリーとパイプラインの追加)」を選択します。これで、開発およびデプロイ作業のすべてをブラウザー内で行えるようになります。

初めての Clojure

Clojure は、JVM 上または JavaScript 内で実行できる、Lisp のダイアレクトです。Clojure を Node.js アプリケーションから実行するには、clojurescript-nodejs パッケージを使用します。

  1. packages.json ファイルを編集して、パッケージのリストに clojurescript-nodejs を追加します。
    "dependencies": {
        "express": "4.13.x",
    	"cfenv": "1.0.x",
    	"clojurescript-nodejs": "*"
    },
  2. Clojure 環境を作成します。
    // Get a Clojure environment
    var cljs = require('clojurescript-nodejs');
  3. Clojure 関数を定義します (以下のコードの詳しい説明については、次のセクションを参照してください)。
    cljs.eval('(ns app (:require clojure.string)) ' +
    	'(defn h2 [str] (clojure.string/join ["<h2>" str "</h2>"]))');
  4. Clojure コードを使用して URL リクエストに応答します (以下のコードの詳しい説明については、次のセクションを参照してください)。
    app.get("/trivial", function(req, res) {
    res.send(cljs.eval('(ns app) (h2 "Hello, Clojure")'));
    });
  5. アプリケーションを実行して、結果を確認します。

Clojure 関数が機能する仕組み

Clojure の知識がある場合、または Lisp の他のダイアレクトに関する知識がある場合でも、ここで説明する内容はすぐに理解できるはずです。

前のセクションのステップ 3 で定義した cljs.eval という Clojure 関数は、ストリングを受け取って、それを Clojure コードとして評価します。この例の場合、ストリングはアポストロフィー () で区切られているので、文字列では引用符 (") を使用することもできます。

cljs.eval('

以下に示す Clojure ステートメントは 2 つの処理を行います。

  1. 名前空間を app として定義します。変数、関数などは名前空間に含まれていなければ定義できないため、これは重要な点です。
  2. clojure.string ライブラリーをインポートします。このライブラリーには、HTML を生成する際に役立つ、ストリングを処理するための関数が含まれています。
(ns app (:require clojure.string))

以下に、実際の関数定義となる Clojure ステートメントを示します。この関数は、初めて定義する関数なので、詳しく見ていきましょう。最初の部分は、関数の名前として h2 を定義します。

(defn h2

以下に示す部分は、この関数の唯一のパラメーターとして str を指定します。この部分では通常の括弧ではなく、大括弧を使用していることに注意してください。Clojure はさまざまな要素を示すために、従来の Lisp とは異なるタイプの括弧を使用します。大括弧はベクトルを定義します。ベクトルはリスト (通常の括弧で囲まれた項目の集合) として機能しますが、リストよりもベクトルを使用したほうが、中央の要素にアクセスするにも、リストの末尾に項目を挿入するにも簡単です。

	[str]

大括弧の後に続く、ライブラリーから関数を呼び出すための構文は、<ライブラリー名>/<関数> です。この部分は、clojure.string ライブラリーから join 関数を呼び出します。この関数は、ベクトルを受け取り、ベクトルに含まれるすべての項目からなるストリングを返します。この例の場合、ストリングは h2 タグで囲まれた形で返されます。

	(clojure.string/join ["<h2>" str "</h2>"])

関数定義全体はリストであり、最後の括弧がリストを終了します。

)

前のセクションのステップ 4 での app.get 呼び出しに含まれる Clojure コードはさらに単純です。このコードは最初に app 名前空間の一部として自身を識別してから、前に定義された h2 関数を呼び出します。cljs.eval 関数は、最後の式の結果を返します。この例の場合、それに該当するのは h2 呼び出しの結果です。この結果を、res.send 関数が戻り値としてユーザーに送信します。

res.send(cljs.eval('(ns app) (h2 "Hello, Clojure")'));

Clojure でアプリケーション全体を作成する

上記の例よりも高度なコードの場合は、アプリケーション全体を Clojure で作成したほうが簡単です。その場合、JavaScript 内の小さなスタブだけを使用してアプリケーションを作成できます。

JavaScript スタブ

以下に、最も基本的な app.js について説明します。このファイルの最初の行は、エラーであると思われないようにするための編集者への指示です。

/*eslint-env node*/

次に、Clojure 環境を作成します。

// Get a Clojure environment
var cljs = require('clojurescript-nodejs');

ここではストリングを評価するのではなく、ファイルを評価します。

// Evaluate the application library
cljs.evalfile("app.cljs");

Clojure アプリケーション

app.cljs の最初の部分は、テンプレートが付随する app.js と同等です。以下に、行ごとの内容を説明します。

以下の行は、名前空間 (my-ns) を定義して、必要な 2 つのライブラリー (clojure.stringcljs.nodejs) をインポートします。最初のライブラリーはそのままインポートしますが、2 番目のライブラリーは :as node を使用してベクトル内に格納することで、名前空間内の残りの部分にあるコード内ではこのパッケージを node として参照するように指定します。

(ns my-ns (:require clojure.string
			[cljs.nodejs :as node]
	   )
)

Clojure でのコメントは、セミコロン (;) で始まります。

; Get the application environment

これに続く行は、基礎となる JavaScript と通信するために Clojure で使用する 2 つのメカニズムを示しています。js/ 構文は、JavaScript のグローバル要素 (require など) にアクセスする場合に使用します。スニペット (js/require "cfenv") は、require("cfenv") と同等です。Clojure でオブジェクトのメソッドを呼び出すには、(.<メソッド> <オブジェクト> <パラメーター (必要な場合)>) の構文を使用します。スニペット (.getAppEnv (js/require "cfenv")) は、require("cfenv").getAppEnv() と同等です。

def はグローバル変数を定義する要素です。この要素は 2 番目のパラメーターの値を計算して、その結果を最初のパラメーターに代入します。以下の行は、var appEnv = require("cfenv").getAppEnv() と同等です。

(def appEnv (.getAppEnv (js/require "cfenv")))

以下の行は、メソッド呼び出しが欠けていることを除き、appEnv の行と同様です。

; Create an express application
(def express (js/require "express"))

以下の行は、JavaScript にアクセスするもう 1 つの方法を示しています。(js* <>) は、指定の JavaScript 式を実行し、その結果の値を返します。この行は、var app = require("express")() と同等です。

(def app (js* "require('express')()"))

以下の行は、JavaScript を使用するための別のメカニズムを示しています。オブジェクトがある場合、構文 (aget <オブジェクト> <属性>) を使用することで、そのオブジェクトの属性を取得できます。構文 (.<メソッド> <オブジェクト> <パラメーター (必要な場合)>) と組み合わせた行全体が、app.use(express.static("public")) と同等になります。このコードは、パブリック・ディレクトリー内の静的ファイルを提供するように指定します。

; Static files
(.use app ((aget express "static") "public"))

以下のコードは、例として、パス /trivialn の GET リクエストに対するレスポンスを指定します。匿名関数を作成するための構文は、(fn [<パラメーター>] (<>)) です。この構文を JavaScript メソッドの呼び出し方法と組み合わせると、app.get("/trivial", function(req, res) {res.send("hello")}) と同等になります。

; Respond to a request
(.get app "/trivial" (fn [req res] (.send res "Hello")))

以下のコードは、JavaScript の appEnv で指定されたポート上でアプリケーションの listen を開始するコードと同等です。

; Start listening
(.listen app
	(aget appEnv "port")
	"0.0.0.0"
	(fn []
		(.log js/console (aget appEnv "url"))
	)
)

ユーザーに応答する

ユーザーからの入力内容を無視して常に同じレスポンスを返す Web アプリケーションであっては、あまり役に立ちません。真の Web アプリケーションはユーザー入力を処理する必要があります。ユーザー入力は一般に、パス、クエリー、または HTTP リクエストの本体のいずれかに挿入されるという形式になります。

パスのパラメーター

Express (Node.js の HTTP サーバー・ライブラリー) では、パスのパラメーターを示すために、<キーワード> をパス・コンポーネントとして指定します。その結果は、req.params 内に返されます。以下に、関連するコードについて説明します。

まず、フォーム・ページに戻るためのリンクを使用してストリングを作成します。ユーザーが簡単にフォーム・ページに戻れるようにするためのこのリンクは表面的なものにすぎません。

; Go back to the form
(def goBackForm "<hr /><a href=\"/form.html\">Go back</a>");

/process-path/<何らかの対象> に対する GET リクエストのすべてを受け取る実際の呼び出しは、以下のとおりです。

; Process a part of the path
(.get app "/process-path/:param"

URL がリクエストされると、以下の関数が呼び出されます。この関数は、2 つのストリングを連結して使用して、res.send を呼び出します。具体的には、パラメーター値のストリングと、(前に goBackForm として定義された) フォームに戻るためのリンクに対応する HTML のストリングです。

	(fn [req res]
		(.send res
			(clojure.string/join [

(js* <>) は、指定の式を JavaScript として評価し、その結果の値を Clojure に返すことを思い出してください。したがって、req.params.param などの JavaScript の変数を簡単に読み取ることができます。

				(js* "req.params.param")
				goBackForm
			])
		)
	)
)

クエリーのパラメーター

クエリーのパラメーターは、さらに簡単に処理することができます。パラメーターの名前をコロンで指定する必要はありません。パラメーターの名前は、HTML を基に、ブラウザーによってあらかじめ作成されるためです。

; Respond to GET requests with a query
(.get app "/process-form"
	(fn [req res]
		(.send res
			(clojure.string/join [
				(js* "req.query.get")
				goBackForm
			])
		)
	)
)

本体のパラメーター

(POST および PUT メソッドで使用する) 本体に含まれるパラメーターには、追加の処理が少々必要になります。まず、process-body を packages.json ファイルに追加した上で、このファイルを、エンコードの際に本体のエントリーを処理するミドルウェアとして配置する必要があります。そのステップが完了すると、他の手段でアクセスする場合と同じように、実際のパラメーターにアクセスできるようになります。

; Parse the body
(.post app "*"

この JavaScript の式は、関数を返します。返される関数は、JavaScript と Clojure の両方で、戻り値としても関数パラメーターとしても有効です。

	(js* "require('body-parser').urlencoded({extended: true})")
)

; Respond to POST requests
(.post app "/process-form"
	(fn [req res]
		(.send res
			(clojure.string/join [
				(js* "req.body.post")
				goBackForm
			])
		)
	)
)

すべてのパラメーターを 1 つにまとめる

通常、パラメーターの取得元について気に掛けることはしません。関心の対象となるのは、パラメーターの値だけです。パラメーターにはキーと値があるため、使用するデータ構造としては、マッピングが適切です

以下に、パラメーターのマッピングを取得するコードの例を示します。最初の部分は関数定義です。この関数は、他の場所からでも呼び出せる名前付き関数であるため、fn ではなく defn を使用して定義されています。

; Given a request object, return a map with all the parameters
(defn getParams [req]

let 呼び出しでは、呼び出しの最後にある 1 つ以上の式で使用できるローカル変数をいくつか定義します (大まかに言うと、関数型プログラミングではこれらの変数を実際にはシンボルと呼びます)。この例の場合、定義しているパラメーターは queryMapbodyMapparamMap の 3 つです。

	(let [    ; Define local variables

この 3 つすべての定義では js->clj 関数を使用して、JavaScript オブジェクトを Clojure のマッピングに変換しています。

		queryMap (js->clj (js* "req.query"))
		bodyMap (js->clj (js* "req.body"))
		paramMap (js->clj (js* "req.params"))		
	]

merge 関数は、3 つの異なるマッピングを取り、それらを 1 つのマッピングにマージします (キーが共通するパラメーターがある場合は、マージ後のマッピングに含まれる値がマージ前のマッピングに含まれる値をオーバーライドします)。以下の式は let 呼び出しの結果であるため、関数全体の結果ということになります。

	(merge queryMap bodyMap paramMap)
	)   ; end of let
) ; end of defn

リクエストに実際に応答する呼び出しは以下のとおりです。

; Respond requests with parameters from all over
(.all app "/merge/:pathparam"
	(fn [req res]

以下に、let の別の使用法を示します。命令型プログラミングに携わった経歴を持つ私にとっては、使用するシンボルをあらかじめ定義して処理の大半を行ってから、関数を作成して理解するほうが簡単です。

		(let [ ; local variables

前に定義した関数を呼び出して、パラメーターを取得します。

				params (getParams req)
			]
			(.send res
				(clojure.string/join [

この時点で、JSON.stringify を使用してマッピングを JSON に変換する必要があります。そのためには、clj->js を使用してマッピングを JavaScript のオブジェクトに戻します。

					(.stringify js/JSON (clj->js params))
					goBackForm
					])  ; end of clojure.string/join
			)  ; end of res.send
		)	; end of let
	) ; end of fn [req res]
)   ; end of app.all

他の API を使用する場合のチートシート

Node.js および Express パッケージに使用するツールは、他のあらゆる API やパッケージにも使用できます。

  • パッケージを使用するには、(def <変数名> (js/require "<パッケージ名>")) を使用します。
  • メソッドを呼び出すには、(.<メソッド> <オブジェクト> <パラメーター (必要な場合)>) を使用します。
  • JavaScript のグローバル変数を使用するには、js/<変数> を使用します。
  • JavaScript の式を評価するのに他のすべてが駄目なら、構文 (js* "<JavaScript expression>") を使用します。

まとめ

このチュートリアルでは、Clojure で Web アプリケーションを作成する際の基礎を説明しました。次のステップは、関数型プログラミングの機能を実際に使用して、アプリケーションを改良することです。その方法を主題にした書籍はいくつもあります。ちなみに、私のお気に入りは『The Joy of Clojure』(Michael Fogus、Chris Houser 共著) です。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing, Web development
ArticleID=1052888
ArticleTitle=IBM Cloud 上で Clojure Web アプリを作成する
publish-date=11302017