本稿の目的
- プログラミング言語の進化について考察する
- 事例としてはLISPの方言を採用する
前提
本稿における進化
- 進化の定義
- 形質が世代を経る中で変化していく現象
- 進化=発展という解釈はしない
- 形質が世代を経る中で変化していく現象
- 各言語の初期の設計を中心に分析を試みる
- 以下は次回以降の課題とする
- 各言語のバージョンアップに伴う進化
- ライブラリの進化
- 開発環境の進化
- 言語使用者の文化の進化
- ただし、処理系の進化については適宜触れる
- 以下は次回以降の課題とする
LISPを取り上げる理由
- 1958年に初めて設計され、高水準言語では2番目の歴史を持つ
- 1番目はFORTRAN
- 多数の方言を持つ
- 今なお、現役で利用されている
方言採用基準
- 積極的に使用されている
- 特徴的な構文を持つ
今回取り上げる方言
- Scheme
- Common Lisp
- Clojure
- LFE(Lisp Flavored Erlang)
- Hy
今回取り上げない方言
- Emacs Lisp
- 特徴的な構文はダイナミックスコープのみ
- それ自体も単に実装の簡略化のためとされている
- また、最近はレキシカルスコープが主として使われている
- 特徴的な構文はダイナミックスコープのみ
- Arc
- 言語自体があまり積極的に使われていない
- 言語設計自体は特徴的
- 言語自体があまり積極的に使われていない
LISP
歴史
- LISt Processorの略として名付けられた
- プログラム自身がリストというデータ構造でできている
- 人工知能研究やプログラミング言語の研究でよく用いられていた
- 多くの言語機能を最初に考案した
- 高階関数
- ガベージコレクション
- 動的型付け
- コンパイラ
Hello World
- LISPはかっこ(リスト)でデータを囲む
- 囲まれたリストをS式(S-expression)と呼ぶ
- 関数を実行する場合は、S式の先頭にシンボル(関数名)を置く
- 前置記法と呼ばれる
- 以下、LISPのコードはすべてCommon Lispで記述
(print "Hello, World!")
演算子
- 演算子も中置記法ではなく、前置記法
- すべてかっこで囲むため、優先順位の概念はない
- 可変長の引数を受け取れる
;; コメントはセミコロンのことが多い
;; 加算
(+ 1 2) ; => 3
(+ 1 2 3 4 5) ; => 15
;; 論理演算
(and 1 2 3 4 5) ; => 5
(or 1 2 3 4 5) ; => 1
;; 入れ子
(+ (- 10 5)
(* 3 10)) ; => 35
関数
- 関数はファーストクラス
- 関数定義も無名関数に名前を付けて束縛という形をとることが多い
- 最後に評価した式を返り値として返す
- 明示的なreturnはほとんどしない
- 基本的には動的型付け
;; 加算関数
;; xとyを引数として束縛している
(defun adder(x y)
(+ x y))
(adder 1 2) ; => 3
;; 無名関数を束縛する
(setf (symbol-function 'adder)
(lambda (x y) (+ x y)))
(adder 1 2) ; => 3
リストとクォート
- リストを関数ではなく、データとして評価したいときはクォート(quote)する
- クォートしたいリストの前に'(シングルクォーテーション)をつける
- クォートされたデータは評価されない
- クォートされたデータを再評価するときはevalする
- リストの先頭を取り出すにはcar、残りを取り出すときはcdrを使う
;; クォートされたリスト
'(1 2 3 4) ; => (1 2 3 4)
;; クォートされていれば関数は評価されない
'(+ 1 2 3 4 5) ; => (+ 1 2 3 4 5)
;; 再度評価したければevalすればよい
(eval '(+ 1 2 3 4 5)) ; => 15
;; リストの分解
(car '(1 2 3 4)) ; => 1
(cdr '(1 2 3 4)) ; => (2 3 4)
;; 関数のシンボルも取り出せる
(car '(+ 1 2 3 4 5) ; => +
S式の生涯
- Lispの式は3段階で実行される
- リード(読み込み)
- 文字列を字句解析して、式を組み立てる
- コンパイル
- 実行
- リード(読み込み)
;; 文字列からの読み込み
(read-from-string "(a b c)") ; => (A B C)
(read-from-string "'(a b c)") ; => '(A B C)
(read-from-string "6/8") ; 3/4
マクロ
- 一般的にはコンパイル時に動的にコードを書き換える機能を指す
- リード時やマクロ展開後に動作するマクロもある
- for、while、when、ifなどの制御構造はマクロで実装可能
- 展開形はmacroexpandなどで確認できる
;; 逆順に評価する
(defmacro reverse-list(&rest lst)
(reverse lst))
(reverse-list 1 2 3 +) ; => 6
;; ifがあればwhenは自分で作れる
(defmacro when (test &body body)
`(if ,test
(progn
,@body)))
;; マクロ展開形
(macroexpand-1
'(reverse-list 1 2 3 +)) ; => (+ 3 2 1)
;; リード時は展開されていない
(read-from-string "(reverse-list 1 2 3 +)") ; => (REVERSE-LIST 1 2 3 +)
Scheme
歴史
- 1975年頃に設計される
- 大学や研究者の間で主に広まる
- 共通の仕様が1984年より作られ、現在も改定されている
- RnRS
- The Revised Report on the Algorithmic Language Scheme
- nにはバージョンが入る
- 現在はR7RSが最新
- バージョンごとの互換性がない機能がある
- RnRS
理念
RnRSより
プログラミング言語の設計は、機能の上に機能を積み重ねることによってではなく、余分な機能が必要であるように思わせている弱点と制限を取り除くことによってなされるべきである
シンプルな言語構造
- シンプルな文法
- R5RSまでは仕様書は50ページ以内に納まっていた
- 現在の関数に必須の機能の多くを早い段階で備えていた
- 関数がファーストクラス
- レキシカルスコープ
- 末尾呼出し最適化
;; 階乗
(define factorial
(lambda(n)
(if (= n 0)
1
(* n (factorial (- n 1))))))
特徴
- Lisp-1
- 関数と変数で共通の名前空間を使う
- マクロやオブジェクトモデルを使った時に制約がかかる場合がある
- そうでないものはLisp-2と呼ばれる
- 関数と変数で共通の名前空間を使う
- 健全なマクロ
- 継続
Schemeのマクロ
- 健全なマクロ(Hygienic macros)
- マクロ展開した結果、変数のシンボルの衝突を回避する
- アナフォリックなマクロも頑張れば書けるらしい
- 複数のパターンにマッチングさせられる
- 局所的なスコープのマクロがある
;; シンプルなケース
(define-syntax while
(syntax-rules ()
((_ condition . body)
(let loop ()
(cond (condition
(begin . body)
(loop)))))))
;; 複数パターン
(define-syntax incf
(syntax-rules ()
((_ n) (begin (set! n (+ n 1)) n))
((_ n i) (begin (set! x (+ n i)) n))))
継続(Continuation)
- トップレベルに戻るための計算過程をファーストクラスとして扱える
- 継続は計算の未来全体を表している
- 継続を使いたいときはcall-with-current-continuation関数を呼び出す
- 省略名はcall/cc
- 現在の継続を生成し、それを関数の引数として渡してその関数を実行する
(define *saved* ())
;; (+ 1 x)という式を保存していると考える
(+ 1 (call/cc (lambda(cc)
(set! *saved* cc) 2))) ; => 3
(*saved* 5) ; => 6
継続でできること
- ループ
- 継続を使うことで状態を巻き戻せるのでループを作れる
- ループから脱出
- 継続を使えばトップレベルに強制的に戻れる
- バックトラッキング
- 過去の状態に戻りながら、非決定性の問題を解く
;; ループから脱出
(call/cc
(lambda (break)
(for-each (lambda (n)
(if (even? n)
(break n)))
'(11 9 10 5))
#t)) ; => 10
Schemeと処理系の話
- 共通の仕様はあるが、処理系によってできることが大きく違う
- 業務で使うには当初のSchemeの仕様は小さすぎたためか
- 現状のR7RSでは大きなライブラリが検討されている
- 仕様はバージョンごとに完全に互換があるわけではないのも大きい
- 業務で使うには当初のSchemeの仕様は小さすぎたためか
- 1つの言語として分岐した言語もある
- Racket
主なScheme処理系
- Racket
- 海外でよく使われている
- 十分なエコシステムを最初から用意
- マクロシステムが強力
- 独自言語を作れることを推しにしている
- コンパイラを触るためのAPIなど
- 独自言語を作れることを推しにしている
- mixin クラスシステムを持つ
- Gauche
- CLOSに近いオブジェクトシステムを持つ
- マルチスレッドサポート
- 低レイヤーにも対応している
- 他にSagittarius、Chickenなどがある
- Chickenは文化的にオブジェクト指向を推奨
Racketのオブジェクトシステム
- Rubyライクなオブジェクトシステム
- メッセージ送信によるOOP
- 元々のSchemeにはOOP的な仕組みは用意されていない
;; クラス定義
(define point%
(class object%
(super-new)
(init-field [x 0] [y 0])
(define/public (move-x dx)
(new this% [x (+ x dx)]))
(define/public (move-y dy)
(new this% [y (+ y dy)]))))
;; メッセージ送信
(send+ (new point%)
(move-x 5)
(move-y 7)
(move-x 12))
公式ドキュメントより
Gaucheのオブジェクトシステム
- CLOSに近い
- クラスとメソッドが分離している
- 実行されるメソッドは動的に選択される
(define-class point ()
((x :init-value 0)
(y :init-value 0))
)
(define-method move ((p point) dx dy)
(inc! (slot-ref p 'x) dx)
(inc! (slot-ref p 'y) dy))
(define-method write-object ((p point) port)
(format port "[point ~a ~a]"
(slot-ref p 'x)
(slot-ref p 'y)))
Schemeまとめ
- 当初は小さな仕様を標榜していたが、最近は大きい仕様も認める流れになってきている
- 処理系によって文化、機能が独自発展している
- ただし、小さな構文の組み合わせで大きな機能を提供するという考え自体はなお残っている
Common Lisp
歴史
- 1994年にANSIによって規格化された言語
- 以後、改定はほぼされていない
- MACLISP系の方言の標準化、発展を目指して作られた
- 仕様は1000ページを超えており、かなり大きい
特徴
- 強力なマクロ機能を持つ
- リーダーマクロ
- コンパイラマクロ
- マルチパラダイム言語
- 巨大なオブジェクトシステム(CLOS)
- 参照透過なAPIが標準で用意
- 同時に破壊的な操作もサポートしており、手続き的なスタイルも可能
リーダーマクロ
- S式を読み込むタイミング(リード時)に式を変換できる
- 通常のマクロ展開の前
- 新しい記号を言語に追加し、解釈させることができる
- 例えば、'(クォート)はリーダマクロで、読み込み時にquote関数に展開されている
- '(1 2 3)
- (quote 1 2 3)
- '(1 2 3)
- []、{}のように囲む記号も定義可能
- 例えば、'(クォート)はリーダマクロで、読み込み時にquote関数に展開されている
;; リーダーマクロの定義
;; quoteの定義
(set-macro-character #\'
#'(lambda (stream char)
(list 'quote (read stream t nil t))))
リーダーマクロでできることの例
- メタデータの付与
- JavaScriptのように{}でハッシュを作成する
- #[1 5]で特定範囲のシーケンスを作成する
;; cl-annotの例
;; Javaのアノテーションのように情報を付加できる
@export-slots
(defclass foo ()
(bar baz))
;; 展開系
(progn
(export '(bar baz))
(defclass foo ()
(bar baz)))
コンパイラマクロ
- 通常のマクロ展開(コンパイル時)が終わった後に動作する
- 関数やマクロの式を受け取り、最適化することが可能
- パラメータが定数である場合にインライン化してしまうなど
- 実行時のパフォーマンスが向上する
- パラメータが定数である場合にインライン化してしまうなど
(defun square (x) (expt x 2))
(define-compiler-macro square (&whole form arg)
(if (atom arg)
`(expt ,arg 2)
(case (car arg)
(square (if (= (length arg) 2)
`(expt ,(nth 1 arg) 4)
form))
(expt (if (= (length arg) 3)
(if (numberp (nth 2 arg))
`(expt ,(nth 1 arg) ,(* 2 (nth 2 arg)))
`(expt ,(nth 1 arg) (* 2 ,(nth 2 arg))))
form))
(otherwise `(expt ,arg 2)))))
(square (square 3)) ; => 81
(macroexpand '(square x)) ; => (SQUARE X), false
CLOS(Common Lisp Object System)
- 巨大なオブジェクトシステム
- 特徴
- 実行中にクラスの再定義、オブジェクトのクラスの再定義が可能
- 多重継承が可能
- 多重ディスパッチ
- 複数のパラメーターに基づいて実行するメソッドを選択
クラスの定義
- クラスはデータ構造とそのアクセサから成る
;; クラスの定義
;; ageとnameというスロット(属性)を定義
(defclass animal ()
(age name))
;; 継承(スロットも継承している)
(defclass dog (animal)
(tail))
;; インスタンス作成
(setf the-dog (make-instance 'dog))
;; スロットの設定
(setf (slot-value the-dog 'name) "Masaru")
;; スロットの読み込み
(slot-value the-dog 'name) => Masaru
メソッドの定義
- 一般的なOOPとは異なり、クラスとメソッドは一体ではない
- メソッドはクラスとは別に定義される
- 型を引数に取る独立した関数
- クラスによるポリモーフィズムや継承関係が実現できる
- 同名で定義した関数を動的に選択して実行
- マルチディスパッチ
- 同名で定義した関数を動的に選択して実行
;; メソッドの設定
(defmethod your-name ((the-dog dog))
(format t "Hello, ~A~%"
(slot-value the-dog 'name)))
(your-name the-dog) ; => Hello, Masaru
;; 同名のメソッドを設定できる
(defmethod your-name ((the-animal animal))
(format t "Good evening, ~A~%"
(slot-value the-dog 'name)))
;; 優先順位に基づいて、先に定義したメソッドが実行される
;; 最も特定的なものが呼ばれる
(your-name the-dog) ; => Hello, Masaru
CLOSその他
- 事前処理、事後処理などもかける
- :around、:before
- メソッドからメソッドを呼ぶ戦略を自由に定義できる
- メソッド・コンビネーション
(defmethod your-name ((the-dog dog))
(print
(format t "Hello, ~A~%"
(slot-value the-dog 'name))))
;; 事前処理
(defmethod your-name :before ((the-dog dog))
(print "before greeting"))
;; 事後処理
(defmethod your-name :after ((the-dog dog))
(print "after greeting"))
(your-name the-dog)
;; > before greeting
;; > Hello, Masaru
;; > after greeting"
その他Common Lispの特徴
- formatが多機能
- ループができる
- 再帰ができる
- 単数形・複数形の変換ができる
- loopが多機能
- シーケンスのループ
- 複数の変数のカウントアップ
- when、if、whileなどを内包できる
- 最適化する方法が用意されている
- 最適化宣言
- 型宣言
(format nil "~@R ~(~@R~)" 14 14) ; => "XIV xiv"
(format nil "~D tr~:@P/~D win~:P" 1 3) ; => "1 try/3 wins"
(format nil "~R dog~:*~[s are~; is~:;s are~] here." 3) ; => "three dogs are here."
(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
;; 最適化宣言
;; スピードをエラーチェックより優先する
(declaim (optimize speed (safety 0)))
;; 型宣言
(defun f (x y)
(declare (type fixnum x y))
(let ((z (+ x y)))
(declare (type fixnum z))
z))
Common Lispまとめ
- ユーザーに自由を与えるための仕様が充実している
- マルチパラダイム
- 強力なマクロシステム
- 最適化オプション
- 一つ一つの機能が多機能に設計されている
- 数値
- CLOS
- ライブラリ
Clojure
歴史
- 2007年から始まった比較的新しい言語
- 安定した環境で動く、マルチスレッドプログラミング言語を目指して作られた
特徴
- JVM上で動作する
- Javaを利用できるし、JavaからもClojureのコードを呼び出せる
- マルチスレッドを意識した設計がされている
- 関数型プログラミング
- 非同期処理を前提にした状態管理
- データ型が丁寧に設計されている
関数型としての特徴
- 豊富なデータ構造があり、すべてイミュータブル
- 再帰の最適化がかかる
- loop/recur構文を使う必要はある
;; mapを定義
(def x {:a 1 :b 2 :c 3})
;; 属性の追加
(assoc x :d 4) ; => {:a 1, :b 2, :c 3, :d 4}
;; 元のmapは不変
x ; => {:a 1, :b 2, :c 3}
;; loopを使った階乗
(defn factorial [x]
(loop [n x f 1]
(if (= n 1)
f
(recur (dec n) (* f n)))))
分配束縛(Destructuring)
- データ構造を分解して、変数に束縛できる
- let(ローカル変数)や関数の引数を分配するときに使う
(let [[a b c d] [1 2 3 4]]
(println a b c d)) ; => 1 2 3 4
(let [[head & tail] [1 2 3 4]]
(println tail)) ; => (2 3 4)
(let [{a :a b :b} {:a 1 :b 2}]
(println a b)) ; => 1 2
Clojureの状態管理
- 並行プログラミングを前提としている
- スレッドとロックを意識しない
- 4つの異なる並行処理モデルを持つ
- スレッドごとに状態を持つ(var)
- 同期的に更新する(atom)
- トラザクション内で管理する(ref)
- 簡易的に非同期で更新する(agent)
; 作成
(def a (ref '(1 2 3)))
; 読み込み
(deref a) ; -> (1 2 3)
; 状態の変更はdosyncの中でのみ許される
(dosync (ref-set a '(3 2 1)))
;; dosyncを使わない状態の変更はエラーになる
;; (ref-set a '(3 2 1))
;; 状態が変更されている
(deref a) ; -> (3 2 1)
Clojureのオブジェクトシステムの特徴
- 基本的にはdefrecordによって型を定義する
- defrecordで生成されたオブジェクトはほぼmapと同一視できる
- キーによるアクセス、新しいキーの追加など
;; クラス定義
(defrecord Person [fname lname])
;; クラス
(def p-rec (Person. "Stu" "Halloway"))
;; map
(def p-map {:fname "Stu", :lname "Halloway"})
;; キーによるアクセス
(:fname p-rec) ; => Stu
;; mapとほぼ同一視できる
(= (:fname p-rec)
(:fname p-map)) ; => true
;; フィールドを置き換えたオブジェクトの取得
(assoc p-rec :fname "Stuart") ; => #user.Person{:fname "Stuart", :lname "Halloway"}
(assoc p-map :fname "Stuart") ; => {:fname "Stuart", :lname "Halloway"}
型によるポリモーフィズム
- 型によるポリモーフィズムが可能
- 一般的なインターフェースの継承
- Clojureのクラス定義はインターフェースのみ継承可能
- 一般的なインターフェースの継承
- Javaの既存クラスを拡張するときに使うことが多い
- マルチメソッドよりパフォーマンスの面で有利
;; インターフェース定義
(defprotocol AnimalProtocol
(bark [this]))
;; インターフェースを継承した実装型
(defrecord Dog []
AnimalProtocol
(bark [this] "Wan"))
(defrecord Duck []
AnimalProtocol
(bark [this] "Gwa"))
(extends? AnimalProtocol Dog) ; => true
(extends? AnimalProtocol Duck) ; => true
;; メソッド呼び出し
(bark (new Dog)) ; => Wan
(bark (new Duck)) ; => Gwa
;; 既存クラスも動的にインターフェースを継承できる
(extend String AnimalProtocol
{:bark (fn [this] "Nya")})
(bark "Hoge") ; => Nya
値によるポリモーフィズム
- 多重ディスパッチによるポリモーフィズムができる
- マルチメソッドと呼ばれる機能を使う
- 分岐する方法は自分で定義できる
- Common LispのCLOSは型の種類による分岐
- こちらは型を含め自由
- Common LispのCLOSは型の種類による分岐
- 継承関係(ヒエラルキー)を自分で定義することもできる
;; 総称関数定義
(defmulti factorial identity)
;; メソッドの定義
(defmethod factorial 0 [_] 1)
(defmethod factorial :default [num]
(* num (factorial (dec num))))
(factorial 0) ; => 1
(factorial 1) ; => 1
(factorial 3) ; => 6
(factorial 7) ; => 5040
Clojureのマクロ
- `(バッククォート)すると名前解決される
- gensymのリーダーマクロ(#)がある
- ユニークなシンボルを作成する
- リーダーマクロは自分で定義できない
- やる方法がないわけではないが、推奨されていない
(defmacro and
"Evaluates exprs one at a time, from left to right. If a form
returns logical false (nil or false), and returns that value and
doesn't evaluate any of the other expressions, otherwise it returns
the value of the last expr. (and) returns true."
{:added "1.0"}
([] true)
([x] x)
([x & next]
`(let [and# ~x]
(if and# (and ~@next) and#))))
公式リポジトリより
Clojureまとめ
- LISPの思想や歴史を踏まえつつ、新しい方向性を模索している
- 最近のパラダイムを強制しており、従来より自由度を減らす方向で設計されている
- 誰にでも使いやすい存在を目指しているような機能追加が多い
LFE(Lisp Flavored Erlang)
歴史
- Clojureと同じくこちらも2007年ごろに始まったとされる
- 公開されたのは2008年
- 作った動機
- LisperのErlang作者の一人が仕事でプログラミングをしておらず、何か適当な仕事を探していた
理念(LFE Style Guide)
- 基本的にはErlang Wayを継承する
- 関数型で記述する
- より安全な方法で書くべき
- 複雑なマクロは書かない
- マクロシステムも比較的普通
特徴
- Lisp Flavored Erlangという名前の通り、Erlangの思想が強い
- パターンマッチング
- メッセージパッシング
- エラーハンドリング
- BEAM(VM)上で動き、OTPを自由に使える
- Ealangともシームレスに統合
- 関数の呼び出しコストがかからない
- Ealangともシームレスに統合
パターンマッチング
- あらゆる場面でパターンマッチングを使用できる
- 関数定義
- case文
- let(ローカルスコープ)
- メッセージのreceive
- マクロ定義
- ガード節(when)を定義して、制限事項も定義できる
- パターンマッチング内でキーワードが使用可能
- consやlistなど
(defun list-max
(('() results)
results)
(((cons head tail) result-so-far) (when (> head result-so-far))
(let ((new-result-so-far head))
(list-max tail new-result-so-far)))
(((cons head tail) result-so-far)
(list-max tail result-so-far)))
メッセージパッシング
- プロセスを作って待機させる
- プロセスにメッセージを送り、内容によって処理を実行する
- メッセージ内容はパターンマッチングによって分岐する
- プロセスにメッセージを送り、内容によって処理を実行する
- プロセスを独立させて高度なリアルタイム性と並行性を実現する
(defmodule tut19
(export (start 0) (ping 2) (pong 0)))
(defun ping
((0 pong-pid)
(! pong-pid 'finished)
(lfe_io:format "Ping finished~n" ()))
((n pong-pid)
(! pong-pid (tuple 'ping (self)))
(receive
('pong (lfe_io:format "Ping received pong~n" ())))
(ping (- n 1) pong-pid)))
(defun pong ()
(receive
('finished
(lfe_io:format "Pong finished~n" ()))
((tuple 'ping ping-pid)
(lfe_io:format "Pong received ping~n" ())
(! ping-pid 'pong)
(pong))))
(defun start ()
(let ((pong-pid (spawn 'tut19 'pong ())))
(spawn 'tut19 'ping (list 3 pong-pid))))
公式ドキュメントより
エラーハンドリング
- Erlangは「Let It Crash」という文化で有名
- 防御的なプログラミングではなく、エラーを前提として復旧の方法を考える思想
- エラーのハンドリングは別プロセスが行う
- 事前にリンクと呼ばれる関連を定義しておく
- エラー発生時にメッセージを別プロセスに送信する
- 事前にリンクと呼ばれる関連を定義しておく
(defmodule tut21
(export (start 1) (ping 2) (pong 0)))
(defun ping (n pong-pid)
(link pong-pid)
(ping1 n pong-pid))
(defun ping1
((0 pong-pid)
(exit 'ping))
((n pong-pid)
(! pong-pid (tuple 'ping (self)))
(receive
('pong (lfe_io:format "Ping received pong~n" ())))
(ping1 (- n 1) pong-pid)))
(defun pong ()
(process_flag 'trap_exit 'true)
(pong1))
(defun pong1 ()
(receive
((tuple 'ping ping-pid)
(lfe_io:format "Pong received ping~n" ())
(! ping-pid 'pong)
(pong1))
((tuple 'EXIT from reason)
(lfe_io:format "Pong exiting, got ~p~n"
(list (tuple 'EXIT from reason))))))
(defun start (ping-node)
(let ((pong-pid (spawn 'tut21 'pong ())))
(spawn ping-node 'tut21 'ping (list 3 pong-pid))))
公式ドキュメントより
LFEのオブジェクトシステムの特徴
- インスタンスはタプルとほぼ同一視できる
- Erlangのレコードから継承した特徴
- レコード定義と同時にいくつかのマクロを生成する
;; レコード定義
(defrecord message-to to-name message)
;; インスタンス作成
(make-message-to message "hello" to-name 'fred)
;; インスタンスは以下のタプルと等価
(tuple 'message-to 'fred "hello")
#(message-to fred "hello")
;; パターンマッチング
(match-message-to to-name the-name message the-message)
;; アクセス
(message-to-message my-message)
;; 設定
(set-message-to-message my-message "goodbye")
公式ドキュメントより
LFEのマクロ
- パターンマッチングと再帰の組み合わせ
(defmacro cond
((list (cons 'else b)) `(progn . ,b))
((cons (cons (list '?= p e) b) cond)
`(case ,e (,p . ,b) (_ (cond . ,cond))))
((cons (cons (list '?= p (= (cons 'when _) g) e) b) cond)
`(case ,e (,p ,g . ,b) (_ (cond . ,cond))))
((cons (cons test b) cond) `(if ,test (progn . ,b) (cond . ,cond)))
(() `'false))
公式リポジトリより
LFEまとめ
- Erlangの文化をベースにLISPの最低限の機能を付け加えた
- 構文的にはCommon Lispがベース
- 極端な自由を忌避して、型にはめるという点ではClojureに近い思想
Hy
歴史
- 2013年に世に出る
- よく使われるLispの中ではかなり新しい
- Pythonとの統合を意識して作られた
- 実際はPythonのASTにコンパイルされる
理念(The Tao of Hy)
- どんな時でもユニコードを使う
- Python2の問題点をできる限り解決する
- 迷った場合はPythonに従う
- それでも迷ったClojureに従う
- それでも迷えばCommon Lispに従う
- それでも迷ったClojureに従う
特徴
- Python、Clojure、Commmon Lispの影響を受けている
- Pythonのデータ構造
- Clojureのライブラリと構文
- Commmon Lispのマクロシステム
Pythonから受け継いだもの
- データ構造
- リスト、辞書、集合、クラスなど
- デコレーター
- 関数を外側からラップする
;; クラス定義の説明
(defclass Cat []
[age None
colour "white"]
(defn speak [self] (print "Meow")))
(setv spot (Cat))
(setv spot.colour "Black") ; => 'Black'
(.speak spot) ; => Meow
公式ドキュメントより
デコレーター
(defn inc-decorator [func]
(fn [value-1 value-2] (func (+ value-1 1) (+ value-2 1))))
(defn inc2-decorator [func]
(fn [value-1 value-2] (func (+ value-1 2) (+ value-2 2))))
;; シンプルなデコレーター
(with-decorator inc-decorator (defn addition [a b] (+ a b)))
(addition 1 1) ; => 4
;; デコレーターを重ねる
(with-decorator inc2-decorator inc-decorator
(defn addition [a b] (+ a b)))
(addition 1 1) ; => 8
公式ドキュメントより
Clojureから受け継いだもの
- 基本的な構文
- 細かなAPI
- マクロ
- ->、as->、loop/recur……
- シーケンス系のAPI
- butlast、partition、cycle……
- マクロ
- 遅延シーケンス
;; 遅延シーケンス
(defseq fibonacci [n]
"infinite sequence of fibonacci numbers"
(cond [(= n 0) 0]
[(= n 1) 1]
[True (+ (get fibonacci (- n 1))
(get fibonacci (- n 2)))]))
公式ドキュメントより
ポリモーフィズム
- マルチメソッドによる多重ディスパッチができる
- Clojureのそれとほぼ同じ
;; 面積
(defmulti area [shape] (:type shape))
(defmethod area "square" [square]
(* (:width square) (:height square)))
(defmethod area "circle" [circle]
(* (** (:radius circle) 2) 3.14))
(default-method area [shape] 0)
(area {:type "circle" :radius 0.5}) ; => 0.785
(area {:type "square" :width 2 :height 2}) ; => 4
(area {:type "non-euclid rhomboid"}) ; => 0
公式ドキュメントより
Common Lispから受け継いだもの
- マクロシステム
- リーダーマクロ
- deftag
- リーダーマクロ
- Common Lispの有名な著作より
- On Lisp(Paul Graham)
- アナフォリックマクロ
- ap-if、ap-each
- アナフォリックマクロ
- Let Over Lambda(Doug Hoyte)
- マクロを定義するマクロ
- defmacro/g!、defmacro!
- マクロを定義するマクロ
- On Lisp(Paul Graham)
;; リーダーマクロ
(deftag ♣ [expr] `[~expr ~expr])
;; 配列のペアを作る
#♣ 5 ; => [5, 5]
;; 副作用も扱える
(setv x 0)
#♣(+= x 1) ; => [None, None]
x ; => 2
;; アナフォリックマクロ
(ap-each-while [1 2 3 4 5 6] (< it 4) (print it)) ; => 1 2 3
公式ドキュメントより
独自と思われるもの
- 非同期の関数やループの定義
- for/a、defn/a
- コルーチンを作る
- for/a、defn/a
- キーワードでのみ呼び出せる関数定義
- &kwonly
- 関数型用のアナフォリックマクロ
- ap-pipe、ap-compose
;; 副作用を扱う関数を受け取る
(for/a [element (agen)] (side-effect element))
;; elseも扱える
(for/a [element (agen)] (side-effect element)
(else (side-effect-2)))
;; &kwonly
(defn compare [a b &kwonly keyfn [reverse False]]
(setv result (keyfn a b))
(if (not reverse)
result
(- result)))
(compare "lisp" "python"
:keyfn (fn [x y]
(reduce - (map (fn [s] (ord (first s))) [x y])))) ; => -4
公式ドキュメントより
Hyのマクロ
- 名前解決などはComon Lisp系統
- 構文的なところはClojure寄り
;; g!で始まる変数名にgensym(ユニークなシンボル)を割り当てる
(defmacro defmacro/g! [name args &rest body]
"Like `defmacro`, but symbols prefixed with 'g!' are gensymed."
(setv syms (list
(distinct
(filter (fn [x]
(and (hasattr x "startswith")
(.startswith x "g!")))
(flatten body))))
gensyms [])
(for* [sym syms]
(.extend gensyms [sym `(gensym ~(cut sym 2))]))
`(defmacro ~name [~@args]
(setv ~@gensyms)
~@body))
公式リポジトリより
Hyまとめ
- 様々なLISPの機能を「ユーザーの自由度を上げる」という観点から導入している
- 便利な道具を提供するという点では強欲なLISPといえる
- 文化的にはCommon Lispに近い
- 便利な道具を提供するという点では強欲なLISPといえる
- 独自の機能は元々のアイデアを少し改良したものが多い
- 破壊的な変更や大きな変更をいとわず、実験的・意欲的な動きがみられる
進化に関する考察
自由と制約による分類
- 自由度を高めようという動きと制約をかけようという2つの動きがある
- 自由派
- Common Lisp
- Hy
- パラダイム派
- Clojure
- LFE
- Schemeはどちらともいえない
- 処理系による
- 自由派
- 自由度を上げようとする動きは大きな仕様に拡大していく傾向がある
言語の互換性と設計思想
- 他の言語との互換性重視
- Clojure
- LFE
- Hy
- 必ずしも設計思想は互換性のある言語の文化を強く尊重しているとは言えない
- ClojureとJava
- HyとPython
まとめ
- LISPの進化は2つの動きの止揚である
- 自由度を高めようという動き
- モダンな言語の特徴を取り入れるという動き
- あるパラダイムに特化したもの
- LISPは思想であり、環境とパラダイムはほぼ切り離されている
- Schemeのように最初は同じ思想であっても、別れていくケースもある
- 変化の方向性が同じ言語はほぼない
- 多様なLisperを同一視すると痛い目に合うのでは