はじめに
こんにちは。viviON開発チームの新星()が一人 Fujiと申します。
本日は皆さんに「Lisp」という素晴らしいプログラミング言語を布教しにまいりました。
Lisp is 何
- 忘れ去られた太古の言語
- (かのFORTRANの次、くらいからある)
- 実はAI開発の黎明期に活用されていた
- (極めると、C言語より早いコードを書くこともできると噂されている・・・)
- https://masatoi.hateblo.jp/entry/20161219/1482087363
- https://blog.practical-scheme.net/shiro/20100620a-lisp-speed
- 神の言語 と呼ばれている
- lisp the list processor
- (Lispの記法はすべてがリストというデータ構造からなる)
- 言語仕様がシンプルなため、自分で言語仕様を拡張することができる。これは方言と呼ばれる
- (「俺が考えた最強のLisp」が増えすぎてわけがわからなくなり、現在ではcommon lispやschemeなど一部の方言が主流となった)
Lispを学ぶべき理由
-
https://cruel.org/freeware/hacker.html <ハッカーになろう (How To Become A Hacker)>
- LISP を勉強しておきたいのは、別の理由からです――それをモノにしたときにすばらしい悟り体験が得られるのです。この体験は、その後の人生でよりよいプログラマーとなる手助けとなるはずです。たとえ、実際には LISP そのものをあまり使わなくても。
-
https://www.oreilly.co.jp/books/9784873115870/ <Land of Lisp>
- LISPを習得すると、頭がLISPになる。第二言語としてではなく、LISPで会話し、LISPで設計し、LISPで考え、LISPで夢を見るようになる。
-
https://gigazine.net/news/20140902-if-programming-languages-were-weapons/ <もし16種類のプログラム言語が武器になったら>
- もっとも歴史の古い高水準言語の一つであるLISPは「歯ブラシなどから作った手作りの刃物」。その心は「使ってるヤツはたいていちょっとアブない」から
以上のことから、Lispを学ぶことは技術的な悟りをもたらします。
簡素に「触れたことのないパラダイムを持つ言語を学ぶことで視野を拡張できる」とも言えます。
(もちろん、それはLispに限ったことではありませんが。)
さらには、今時わざわざLispを学ぶ人は(おそらく)少ない ということも魅力と言えます。
よって、逆張りを好む少数派エンジニアの皆さんにとっては、強力な持ちネタ武器ともなってくれることでしょう。
「悟りを開けると聞いて、LISPを勉強している」など人に言おうものなら・・・
(自己責任でお願いします)
学ぶ方法
簡単です。オライリー本「Land of Lisp」を今、今すぐ購入しましょう。日本語版でかまいません。
Lispの歴史、仕組み、環境構築、サンプル、解説、ハンズオン、すべてがここに揃っています!
Fujiも日本語版で所持しています。
入院中でも読破できるほど読みやすいなんて、これは本当にすごい本です!!
「Land of Lispは聖典」
本日はこの聖典の一部を参考に、Lispで原初のWebサーバをつくることで、Lispに触れながらWebサーバの仕組みも一緒に学んでいきます。
素晴らしいことです。Lispよ感謝します。
Lisp
おお Lisp
環境構築
- 適当なLinux(Debian系統)を用意します。
- 下記コマンドでcommon lispをセットアップしましょう。
# セットアップ
sudo apt update
sudo apt install clisp
# 動作確認として対話モード起動
clisp
# これがlispの構文
(* 3 (/ 16 2))
# 終了時
(quit)
実装
- 下記のコードを記載し、「webserver.lisp」として保存します。
- http://landoflisp.com/webserver.lisp より、一部改変してお送りします。
; This program is free software; you can redistribute it and/or modify
; it under the terms of the GNU General Public License as published by
; the Free Software Foundation; version 2 of the License.
;
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
; GNU General Public License for more details.
;
; Partial Author: Conrad Barski, M.D.
; Parts Adapted with permission from http.lisp by Ron Garret
(defun url-decode (s)
(replace (string-upcase s) :test #'char-equal :initial-element #\+ :with #\space))
(defun decode-param (s)
(labels ((f (lst)
(when lst
(case (car lst)
(#\% (cons (code-char (parse-integer (coerce (list (cadr lst) (caddr lst)) 'string) :radix 16 :junk-allowed t))
(f (cdddr lst))))
(#\+ (cons #\space (f (cdr lst))))
(otherwise (cons (car lst) (f (cdr lst))))))))
(coerce (f (coerce s 'list)) 'string)))
(defun parse-params (s)
(let* ((i1 (position #\= s))
(i2 (position #\& s)))
(cond (i1 (cons (cons (intern (string-upcase (subseq s 0 i1)))
(decode-param (subseq s (1+ i1) i2)))
(and i2 (parse-params (subseq s (1+ i2))))))
((equal s "") nil)
(t s))))
(defun parse-url (s)
(let* ((url (subseq s
(+ 2 (position #\space s))
(position #\space s :from-end t)))
(x (position #\? url)))
(if x
(cons (subseq url 0 x) (parse-params (subseq url (1+ x))))
(cons url '()))))
(defun get-header (stream)
(let* ((s (read-line stream))
(h (let ((i (position #\: s)))
(when i
(cons (intern (string-upcase (subseq s 0 i)))
(subseq s (+ i 2)))))))
(when h
(cons h (get-header stream)))))
(defun get-content-params (stream header)
(let ((content (assoc 'content-length header)))
(when content
(parse-params (read-sequence (make-string (read content)) stream)))))
(defun serve (request-handler)
(let ((socket (socket-server 8080)))
(unwind-protect
(loop (with-open-stream (stream (socket-accept socket))
(let* ((url (parse-url (read-line stream)))
(path (car url))
(header (get-header stream))
(params (append (cdr url)
(get-content-params stream header)))
(*standard-output* stream))
(funcall request-handler path header params))))
(socket-server-close socket))))
(defun send-http-response (status-code status-message headers content)
(format t "HTTP/1.1 ~a ~a~%" status-code status-message)
(dolist (header headers)
(format t "~a: ~a~%" (car header) (cdr header)))
(format t "~%~a" content))
(defun hello-request-handler (path header params)
(if (equal path "greeting")
(let ((name (assoc 'name params)))
(if (not name)
(send-http-response 400 "Bad Request" '(("Content-Type" . "text/html; charset=utf-8")) "<form>Lispは神の言語。新たな信者の名前は?<input name='name' /></form>(日本語エンコードは未対応)")
(send-http-response 200 "OK" '(("Content-Type" . "text/plain; charset=utf-8")) (format nil "歓迎します, ~a!" (cdr name)))))
(send-http-response 404 "Not Found" '(("Content-Type" . "text/html; charset=utf-8")) "Sorry... I don't know that page.")))
- 一連のコマンドは下記となります。
touch webserver.lisp
vim webserver.lisp
# <上記コードをコピペする>
clisp
(load "webserver")
(serve #'hello-request-handler)
# コードを編集する場合は、(quit) を実行して対話モードを終了したのち、再度vimからやり直せば反映できる。
- その後、Google Chromeにて「 localhost:8080/greeting 」にアクセスすることでページを確認することが可能です。
ポイント
- lispでもpythonやPHPと同じように、スタンドアロンWebサーバを定義できる。
- common lispにTCP Socketのモジュールがあるため、より簡単に実現可能となっている。
- Webサーバと呼ばれるソフトウェアの実態は、指定したポートでSocket通信を待ち受け→リクエストされてきた文字列を解析→レスポンスの文字列を組んで返却する、という一連の流れを処理するものと言えそう。
- ApacheやNginxなど、普段当たり前のように使っているWebサーバも、分解すれば、上記のように「プログラムが動いて、通信を待ち受け、文字列をよしなにやりとりしてくれるだけ」で基礎部分が成立している。
- よって、リクエストヘッダの解釈や、レスポンスヘッダの返し方などは本来自由に追加カスタムが可能ということ。余裕があればNginx自体のソースコードを読んでみることで、更なる仕組みの理解が可能です。
- セットアップが簡単なため、応用として、軽量Linuxにcommon lispを積み込んでDockerFile化してイメージ化すれば、Lispだけで構築されたWebサービスを構築、公開すること、ECSに搭載することも可能(まさかLispでサービス提供されているとは思うまい・・・)
(おまけ) コード解説
ChatGPTよ 感謝します
このCommon Lispのコードは、HTTPリクエストを処理する非常に単純なウェブサーバーの一部です。以下に、各関数の概要を説明します:
-
url-decode (s):
-
s
として渡された文字列をURLデコードします。 -
%20
がスペースに、+
が+
に変換されます。
-
-
decode-param (s):
- パーセントエンコーディングされた文字列をデコードします。
-
f
というローカル関数が再帰的に処理しています。
-
parse-params (s):
- クエリ文字列(
name=value
の形式)をパースし、アソシエーションリストに変換します。
- クエリ文字列(
-
parse-url (s):
- HTTPリクエストのURLをパースし、パスとクエリパラメータに分割します。
-
get-header (stream):
- HTTPリクエストからヘッダーを取得します。
-
get-content-params (stream header):
- コンテンツのパラメータを取得します。具体的には、Content-Lengthヘッダーを見て、その長さだけのデータを読み取ります。
-
serve (request-handler):
-
HTTPリクエストを受け付け、指定された
request-handler
関数を呼び出します。ループを回してリクエストを継続的に処理します。 -
serve
関数は、TCPソケットを作成し、指定されたポート(ここでは8080)でクライアントからの接続を待機します。 -
接続が確立されると、新しいストリームが作成され、HTTPリクエストの処理が行われます。この処理は
request-handler
関数によって実行されます。 -
このコードでは、Common Lispの
unwind-protect
フォームを使用しています。-
unwind-protect
は、正常終了または例外が発生した場合に必ず実行されるフォームを指定します。この場合、serve
関数全体の処理が終了するか、例外が発生すると、socket-server-close
が呼び出されてソケットが確実に閉じられます。
-
-
loop
フォームは、ソケットが接続を待ち続けるメインのループです。- クライアントからの接続があるたびに新しいストリームが作成され、そのストリームを用いて
request-handler
関数が呼び出されます。リクエストごとに新しいストリームを使用することで、複数のクライアントと同時に通信できます。
- クライアントからの接続があるたびに新しいストリームが作成され、そのストリームを用いて
-
-
send-http-response (status-code status-message headers content):
- HTTPレスポンスを送信します。指定されたステータスコード、ステータスメッセージ、ヘッダー、およびコンテンツを使用してレスポンスを構築します。
-
hello-request-handler (path header params):
- 特定のパスに基づいてレスポンスを生成します。
greeting
パスが来た場合には、名前が提供されていればそれに応じたメッセージを、提供されていなければエラーレスポンスを生成します。それ以外のパスには404 Not Foundを返します。
- 特定のパスに基づいてレスポンスを生成します。
このコードは、HTTPの基本的な要素を処理するサンプルであり、Webアプリケーションの基礎を理解するのに役立ちます。
おわりに
謎の構文、古い歴史、怪しい聖典、現代言語と同様の機能も実現可能なポテンシャル。
お分かりいただけました通り、Lispには夢とロマンがまだまだ詰まっています。
すぐにでも続きを学びたくなってきたのではないでしょうか。
Lispの魅力を少しでもお伝えできていれば幸いです。
以下、少し宣伝させてくださいませ。
弊社viviONでは、Lispと同等以上に夢が詰まったプロダクトがあり、
Lispと同等以上のロマンを持ったエンジニアが現在続々とJoinしています。
もしご興味を持たれましたら、ぜひお声掛けくださいませ。
カジュアル面談も可能です。 ぜひ聖典についてFujiと語りましょう!
(お申し込みの際には「Lispの人と話したい」と一言添えてもらってもOK)
ご観覧賜りまして、誠にありがとうございました。
今後ともよろしくお願い申し上げます。
※なお残念ながら、弊社では開発業務にLispは「一切」使用しておりません。