Zenn
🙆

神の言語といわれるLISPは、数学のように純粋で美しいロジックが展開できる言語。オシャレな LISP インタプリタ。

2024/11/08に公開1

「本記事は、技術的な視点から情報を提供することを目的としております。内容については可能な限り正確性を期しておりますが、記事内の見解は執筆者の意見や理解に基づいており、すべての方にとって普遍的な結論を示すものではありません。技術の分野は常に進化し、新たな知見が追加されることもあります。ご意見がある場合には、建設的な対話を歓迎いたしますが、批判的な意見を展開する際も、お互いの尊重を大切にしたコミュニケーションを心がけていただけると幸いです。」

タイトル: 「東京プログラマー リスプを書かずにはいられない物語」
新宿の夜景を見下ろすカフェで、主人公の大輔は、ノートパソコンに向かって静かにキーボードを叩いていた。彼は東京で働くプログラマー。コードの世界に浸り、特に最近夢中になっているのが関数型言語LISPだった。

LISPは、数学のように純粋で美しいロジックが展開できる言語。大輔は「この関数型の世界がいかにエレガントか」を理解するため、オシャレなリスプインタプリタを使い、毎晩のようにコードを書き込んでいた。今日の挑戦は「階乗を求める関数」を作ることだった。

通常の手続き型言語 であれば 階乗の計算の手順を記述する。リスプの場合は N の場合と N が1の場合を宣言することによって階乗計算 そのものを表現する。

(define (factorial n)
    (if (<= n 1)
        1
        (* n (factorial (- n 1)))))

彼は静かにこのコードを打ち込んだ。リスプインタプリタが、彼のコードを画面上で優雅に解釈していく。「これは…芸術だ」と大輔は思った。階乗の計算とは単純なものに思えるが、この再帰的な構造が作り出すのは美しい計算ステップの世界だった。

インタプリタで factorial 関数を実行し、(factorial 5) と入力してみる。画面に「120」と表示されるのを見て、大輔は思わず微笑んだ。「コードは言葉だ。人の思考パターンを描き出すことができる。」

彼がLISPの世界に夢中になるきっかけは、このオシャレなインタプリタだった。背景が淡い青で、コードは見やすく表示され、エラーもわかりやすい。何より、関数型の奥深い考え方を自然に受け入れることができた。再帰、条件分岐、そして最終的な計算結果。すべてが美しく整然としている。

大輔はカフェの照明が薄暗くなるのを感じたが、気にしなかった。インタプリタの画面に再び集中し、次のコードを書き始めた。「さて、次はもっと複雑な再帰を…」

夜の東京が静かに流れていく中で、大輔の心はコードのリズムに合わせて踊っていた。

第2話 LISPコードの世界
LISPコードの世界に没頭していた。LISPコードは、彼にとってただのプログラムではない。関数型言語特有のエレガントな構造と、計算の美が結晶化された表現だった。

LISPコードとは、単に解を導く手段ではなく、式の評価がそのまま計算の進行そのものである。LISPコードにおいて、解はすでにその内に表現されている。コードとして記述できれば、それが計算可能であり、解が得られるも同様だ。繰り返しや機械的な処理は不要で、すべては再帰構造で構成される。その再帰は、式の中に安定の状態、すなわち最終的な解の条件だけが記述され、計算が実行されると自動的にその安定状態へと収束する。

書き上げたコードは、静かに画面上で計算を進めていく。まるで水が自然に低い場所に流れていくように、計算が無駄なく進行し、安定状態にたどり着く。これは単なる技術ではなく、非常に自然な数学的プロセスそのものだった。

彼の打ち込むLISPコードが、数式の中で確かな秩序を生み出し、全体の計算を統合していく様子は、宇宙の摂理そのもののように思えた。LISPコードの一行一行が、次々と評価され、再帰構造の中で理想の形に到達していく様は、隆司にとっては詩や音楽のような美しさだった。

夜が更け、東の空がほのかに明るくなるころ、彼は画面に浮かび上がる計算結果を見つめ、深い満足感に包まれた。リスプコードは、単に答えを求める道具ではなく、計算の美と理想を追求する彼の到達点だった。

使い方。
コードをメモ帳などのテキストエディタに貼り付け、ファイル名を「index.html」として保存します。その後、保存したファイルをブラウザで開けば、コードが実行されます。

LISPコードを入力して「実行」ボタンを押すと、実行結果がoutputエリアに表示されます。

サンプルコード。
LISPでベクトルの内積を計算するコードです。これは、前述のLISP実行環境で実行できるように書かれています。dot-product 関数は、2つのリストとして表現されたベクトルを引数に取り、それらの内積を返します。

(define (dot-product vectorA vectorB)
    (if (or (null? vectorA) (null? vectorB))
        0
        (+ (* (car vectorA) (car vectorB))
           (dot-product (cdr vectorA) (cdr vectorB)))))

; ベクトル例
(dot-product '(1 2 3) '(4 5 6)) ; 結果は32を期待

コードの説明

dot-product 関数:
vectorA と vectorB が空のリストに到達するまで再帰的に処理します。

各再帰ステップで、car を使って両ベクトルの現在の要素を取得し、掛け算を行ってから累積します。
残りの要素は cdr で取得し、再帰呼び出しを行って最終的な内積を計算します。

使用例:
(dot-product '(1 2 3) '(4 5 6))

前述のLISP実行環境は、@jcubic/lipsというJavaScript製のLISPインタプリタライブラリ「LIPS」を使用しています。これは、LISPの基本的な構文と機能をサポートしており、JavaScript上で動作するためブラウザでも利用できるのが特徴です。

基本的なコードの書き方

LISPは「前置記法」と「S式」(シンボリック式)と呼ばれる構文を使用します。基本の書き方は、すべての演算や関数呼び出しをカッコ () で囲み、先頭に関数名や演算子を置きます。例えば:

(+ 2 3) ; 5を返す
(* 4 5) ; 20を返す
(define x 10) ; xに10を割り当てる
(define (square x) (* x x)) ; 関数定義: 引数xの2乗を返す
(square 4) ; 16を返す

主な組み込み関数と特殊フォーム
LIPSインタプリタの組み込み関数は、標準的なLISP関数に似ていますが、一部JavaScript環境に依存しているものもあります。以下は、よく使うLISPの関数や特殊フォームです。

基本的な算術演算
LISPは、基本の算術演算を前置記法でサポートしています。

(+ 1 2) ; 足し算: 3
(- 5 3) ; 引き算: 2
(* 2 3) ; 掛け算: 6
(/ 10 2) ; 割り算: 5

定義 (define)
変数や関数の定義に使用します。

(define x 10) ; xに10を定義
(define (square x) (* x x)) ; 関数定義
(square 4) ; 結果は16

条件分岐 (if)
条件に応じて処理を分岐させます。

(if (< 3 5) "yes" "no") ; 3が5より小さい場合は"yes"、そうでなければ"no"を返す

リスト操作
LISPのリスト操作に使う関数です。
(list 1 2 3) - リストを生成
(car '(1 2 3)) - リストの最初の要素を取得
(cdr '(1 2 3)) - リストの最初の要素を除いた残りのリストを取得
(cons 1 '(2 3)) - 指定した要素をリストの先頭に追加

論理演算
論理演算もサポートされています。
(and #t #f) - 両方が真のときに真
(or #t #f) - いずれかが真なら真
(not #t) - 真偽値を反転

lambda式
無名関数を作成します。

((lambda (x) (* x x)) 5) ; 結果は25

再帰 (define と if)
再帰的な関数も定義できます。例えば、ベクトルの内積を求める再帰関数:

(define (dot-product vectorA vectorB)
(if (or (null? vectorA) (null? vectorB))
0
(+ (* (car vectorA) (car vectorB))
(dot-product (cdr vectorA) (cdr vectorB)))))

その他のよく使う組み込み関数
null? - リストが空かどうか確認
length - リストの長さを取得
map - リストの各要素に関数を適用
apply - 関数をリストの要素に適用

オシャレな LISP インタプリタのコード。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LISP Code Executor</title>
    <!-- 外部のLISPインタプリタを提供するライブラリを読み込む -->
    <script src="https://cdn.jsdelivr.net/npm/@jcubic/lips/dist/lips.min.js"></script>
    <style>
        /* ページの全体的なスタイルを設定 */
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
            color: #333;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }

        /* 中央のコンテナスタイル */
        .container {
            background-color: #ffffff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            max-width: 600px;
            width: 100%;
        }

        /* 見出しスタイル */
        h2 {
            color: #4a90e2;
            font-size: 1.5em;
            margin-bottom: 10px;
        }

        /* LISPコード入力エリアのスタイル */
        #lispCode {
            width: 100%;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 10px;
            font-family: monospace;
            font-size: 1em;
            resize: vertical;
            background-color: #f7f7f9;
            color: #333;
            box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
        }

        /* 実行ボタンのスタイル */
        button {
            background-color: #4a90e2;
            color: #fff;
            border: none;
            border-radius: 4px;
            padding: 10px 20px;
            font-size: 1em;
            cursor: pointer;
            transition: background-color 0.3s;
            margin-top: 10px;
        }

        /* ボタンのホバースタイル */
        button:hover {
            background-color: #357abd;
        }

        /* 結果表示エリアの見出しスタイル */
        h3 {
            color: #4a90e2;
            margin-top: 20px;
            font-size: 1.2em;
        }

        /* 実行結果表示エリアのスタイル */
        #output {
            background-color: #f7f7f9;
            border: 1px solid #ccc;
            border-radius: 4px;
            padding: 10px;
            font-family: monospace;
            font-size: 1em;
            white-space: pre-wrap;
            min-height: 50px;
            color: #333;
        }
    </style>
</head>
<body>
    <!-- メインコンテナ -->
    <div class="container">
        <h2>LISPコード実行</h2>
        <!-- LISPコード入力エリア -->
        <textarea id="lispCode" rows="8" placeholder="ここにLISPコードを入力してください..."></textarea><br>
        <!-- 実行ボタン -->
        <button onclick="executeLisp()">実行</button>
        <!-- 結果表示エリア -->
        <h3>結果:</h3>
        <pre id="output"></pre>
    </div>

    <script>
        // 初期サンプルコードを設定する
        const sampleCode = `
        (define (factorial n)
            (if (<= n 1)
                1
                (* n (factorial (- n 1)))))
        
        (factorial 5)  ; 120を期待
        `;
        document.getElementById("lispCode").value = sampleCode;

        // LISPコードを実行する関数
        async function executeLisp() {
            // 入力エリアからコードを取得
            const code = document.getElementById("lispCode").value;
            // 結果表示エリアを取得し、初期メッセージをセット
            const output = document.getElementById("output");
            output.textContent = "実行中...";

            try {
                // LISPインタプリタでコードを実行
                const result = await lips.exec(code);
                // 実行結果を表示エリアに反映
                output.textContent = result.toString();
            } catch (err) {
                // エラー発生時にはエラーメッセージを表示
                output.textContent = "エラー: " + err.message;
            }
        }
    </script>
</body>
</html>

追記。サンプルコード集。

前述のLISP実行環境で動作するサンプルコードです。ここでは、数値計算、条件分岐、リスト操作、関数定義、再帰など、LISPの基本操作を示します。

基本的な算術演算

(+ 1 2) ; 足し算、結果: 3
(- 10 5) ; 引き算、結果: 5
(* 3 4) ; 掛け算、結果: 12
(/ 20 4) ; 割り算、結果: 5
(+ (* 2 3) (/ 10 2)) ; 複合演算、結果: 11

変数定義

(define a 10) ; 変数aに10を代入
(define b (+ a 5)) ; 変数bにa + 5を代入、結果: 15

条件分岐と比較演算

(if (> 5 3) "yes" "no") ; 5 > 3の場合は"yes"、結果: "yes"
(if (< 2 1) "yes" "no") ; 2 < 1の場合は"no"、結果: "no"
(if (= 3 3) "equal" "not equal") ; 3 = 3の場合、結果: "equal"

リストの操作

(define my-list '(1 2 3 4 5))

(car my-list) ; 最初の要素を取得、結果: 1
(cdr my-list) ; 最初の要素を除いたリスト、結果: '(2 3 4 5)
(cons 0 my-list) ; 0をリストの先頭に追加、結果: '(0 1 2 3 4 5)
(list 1 2 3) ; 新しいリストを作成、結果: '(1 2 3)
(append '(1 2) '(3 4)) ; リストを結合、結果: '(1 2 3 4)
(length my-list) ; リストの長さを取得、結果: 5

関数定義

(define (square x) (* x x)) ; xの2乗を計算する関数
(square 4) ; 結果: 16

(define (add a b) (+ a b)) ; 2つの引数の和を計算
(add 3 5) ; 結果: 8

再帰関数
階乗の計算

(define (factorial n)
(if (<= n 1)
1
(* n (factorial (- n 1)))))

(factorial 5) ; 結果: 120

フィボナッチ数列

(define (fibonacci n)
(if (<= n 1)
n
(+ (fibonacci (- n 1)) (fibonacci (- n 2)))))

(fibonacci 6) ; 結果: 8

リストの内積計算(再帰を使用)

(define (dot-product vectorA vectorB)
(if (or (null? vectorA) (null? vectorB))
0
(+ (* (car vectorA) (car vectorB))
(dot-product (cdr vectorA) (cdr vectorB)))))

(dot-product '(1 2 3) '(4 5 6)) ; 結果: 32

高階関数 (mapを使用した例)
LISPでは高階関数を使ってリストの各要素に関数を適用できます。

(define (square x) (* x x))

(map square '(1 2 3 4 5)) ; 各要素を2乗、結果: '(1 4 9 16 25)

(map (lambda (x) (* x 2)) '(1 2 3 4 5)) ; 各要素を2倍、結果: '(2 4 6 8 10)

無名関数 (lambda式)

((lambda (x) (* x x)) 5) ; 結果: 25

(define double (lambda (x) (* x 2))) ; 無名関数で2倍にする関数を作成
(double 6) ; 結果: 12

論理演算

(and #t #t) ; 両方が真のときに真、結果: #t
(or #f #t) ; どちらかが真のときに真、結果: #t
(not #t) ; 真偽値を反転、結果: #f

リスト内の最大値を取得する関数

(define (max-list lst)
(if (null? (cdr lst))
(car lst)
(let ((max-rest (max-list (cdr lst))))
(if (> (car lst) max-rest)
(car lst)
max-rest))))

(max-list '(1 5 3 9 2)) ; 結果: 9

素数判定関数

(define (is-prime? n)
(define (check-divisor divisor)
(if (> (* divisor divisor) n)
#t
(if (= (remainder n divisor) 0)
#f
(check-divisor (+ divisor 1)))))
(if (< n 2) #f (check-divisor 2)))

(is-prime? 7) ; 結果: #t (素数)
(is-prime? 10) ; 結果: #f (素数ではない)

リストの合計を計算

(define (sum-list lst)
(if (null? lst)
0
(+ (car lst) (sum-list (cdr lst)))))

(sum-list '(1 2 3 4 5)) ; 結果: 15

リストの要素を逆順にする関数

(define (reverse-list lst)
(if (null? lst)
'()
(append (reverse-list (cdr lst)) (list (car lst)))))

(reverse-list '(1 2 3 4 5)) ; 結果: '(5 4 3 2 1)

リスト内の要素を全て2倍にする関数

(define (double-elements lst)
(if (null? lst)
'()
(cons (* 2 (car lst)) (double-elements (cdr lst)))))

(double-elements '(1 2 3 4)) ; 結果: '(2 4 6 8)

Discussion

O. TowariO. Towari

LISPは最小限を齧った程度ですが、なんちゃっておしゃれ感のあるのはLISPに独特な気がします。なんでしょう、他の言語みたいに必死こいて組むというより、愉しむというのか、なんていうんでしょうね。(笑)

たまにしか使うチャンスがないので、後半のまとめ、ありがたいです。

1箇所、条件分岐の「2 < 1の場合は"no"」は、"yes" ですね。

ログインするとコメントできます