Purple

Purple は Python 仮想マシン上で動作する Lisp 方言です。動的型付けの関数型言語です。

特徴

  • 全ての構文は値を持つ式です。
  • 静的字句スコープです。
  • 同一スコープ内で変数への再代入はできません。
  • ミュータブルなデータ型を使用できますが、イミュータブルなデータ型が基本です。
  • マクロを定義できます。(hygienic macro ではありません)
  • パターンマッチが使えます。
  • Python 3 の itertools、functools、operator モジュールの関数、itertools レシピ相当の関数が組み込みになっています。
  • 既存の Python モジュールを使用できます。
  • デコレータ、キーワード引数、yield など Python 由来の構文を使用できます。(自動部分適用、カリー化などのデコレータ関数が予め定義されています)

Common Lisp や Scheme などの一般的な Lisp と異なる点

  • 「'( )」はいわゆる Lisp の cons セルによるリストではなく Python のタプルの引用です。
  • 「[ ]」はタプル、「{ }」 は不変な辞書(table) のリテラルです。
  • ミュータブルなデータ型のリテラルもあります。「#( )」はリスト(list)、「#{ }」は辞書(dict)のリテラルです。
  • fn 式で無名関数を作れます。また fn 式のショートハンドとして「| |」を使用できます。「| |」は Clojure の「#( )」と同様です。
  • cons セルによるリストを作成するには cell* 関数を使います。


ここからダウンロードできます。
動作環境: CPython (>=3.3)、PyPy (>=3.2.1)

REPL

> purple # もしくは python purple もしくは pypy purple
purple> (def add100 (x)
......   (+ x 100))
<function fun at 0x1018c4c20>
purple> (add100 11)
111
purple>

ファイルの実行

> cat test.purple
(def add100 (x)
  (+ x 100))
(print 11)
> purple test.purple
111
>

ファイルのコンパイル

> purple -c test.purple > test.puc

コンパイル済みファイルの実行

> purple -e test.puc

アトム、四則演算

;整数
3
=> 3
(type 3)
=> <class 'int'>
;type、isinstanceなどのPythonの組み込み関数のほとんどはそのまま使えます。

;文字列
"string"
=> "string"
(type "string")
=> <'str'>

;シンボル
'symbol
;=> symbol

;キーワード
:keyword
;=> :keyword

;真偽値
True
;=> True
False
;=> False

;無
None
;=> None

;四則演算
(+ 1 2)
;=> 3

(* 2 5)
;=> 10

(+ "abc" "def")
;=> "abcdef"

;注意:
;+や-などの演算子は関数ではありません。
;add,sub,mulなどといった、
;各演算子に対応した関数が別にあります。
;これらの関数はPythonのOperatorモジュールの関数です。
;Purpleでは組み込み関数として使えるようになっています。

関数定義、適用、束縛

;グローバル環境でシンボルfooに"foo"を束縛
(val foo "foo")

;引数arg1を表示する関数funを定義
(def fun (arg1)
  (print arg1))

;関数funを適用
(fun foo)
;-> "foo"
;=> None

;ローカル環境(関数定義内)で束縛
foo
;=>"foo"
(def fun2 (arg1)
  (val foo arg1)
  (print foo))
(fun2 "bar")
;-> "bar"
;=> None
foo
;=> "foo"

;ローカル環境で束縛(let)
(let ((foo "a")
      (bar "b"))
  (print foo))
;-> "a"
;=> None
foo
;=> "foo"

;無名関数
((fn (x) (+ x 100)) 11)
;=> 111
(|+ _ 100| 11)
;=> 111
(|+ _1 _2 100| 1 10)
;=> 111

関数適用いろいろ、分配束縛

;可変長引数
;"& "の後ろが可変長引数
(def func1 (first & rest)
  rest)
(func1 1 2 3)
;=> (2 3)

;可変キーワード引数
;"&& "の後ろが可変のキーワード引数
(def func2 (&& kwargs)
  kwargs)
(func2 :arg1 1 :arg2 2)
;=> {:arg1 1 :arg2 2}
;可変キーワード引数はテーブル(不変な辞書) として取得されます。

;引数のキーワード指定
(def func3 (x y)
  y)
(func3 1 :y 2)
;=> 2

(def func4 (x y)
  x)
(func4 :x 1 :y 2)
;=> 1

;分配束縛
(val (one two) '(1 2))
one
;=> 1
two
;=> 2

(val (one & two-three) '(1 2 3))
two-three
;=> (2 3)

(val ((one two) three) '((1 2) 3))
one
;=> 1
three
;=> 3

;letでの束縛も同様に分配束縛可能です。

組み込みデータ型

;タプル
'(1 x 3)
;=> (1 x 3)
(val x 2)
(tuple* 1 x 3) ; ←のショートカットは [1 x 3]
;=> (1 2 3)
[1 x 3]
;=> (1 2 3)
(+ '(1) '(2 3))
;=> (1 2 3)
(car '(1 2 3)) ;firstも同じ
;=> 1
(cdr '(1 2 3)) ;restも同じ
;=> (2 3)
(get '(1 2 3) 0) ; getは実行効率のため関数ではありません。対応する関数はgetitemです。
;=> 1
(cons 1 '(2 3))
;=> (1 2 3)

;Lispスタイルのリンクリスト
(cell* 1 2 3)
;=> L(1 2 3)
(cons 1 '(2 3))
;=> L(1 2 3)
(cdr (cell* 1 2 3))
;=> L(2 3)

;文字列
"string"
(+ "abc" "def" "ghi")
;=> "abcdefghi"
(get "ghi" 0)
;=> "g"
(get "ghi" (slice 0 2))
;=> "gh"

;テーブル(不変な辞書) 
(val t {:first-name "太郎" :last-name "山田"})
(:first-name t)
;=> "太郎"
(t "first-name")
;=> "太郎"
(val t2 {:middle-name "P."})
(:middle-name t2)
;=> "P."
(val t3 (+ t t2)) ; -も使えます。
;=> {:first-name "太郎" :middle-name "P." :last-name "山田"}

;レコード(名前付きタプル)
(record Pos (x y))
(val p (Pos :x 1 :y 2)) ; (val p (Pos 1 2)) でも同じ。
p.x
;=> 1
(getattr p "x")
;=> 1
(get p 0)
;=> 1
(isinstance p Pos)
;=> True
(isinstance p tuple)
;=> True
(isinstance p str)
;=> False

;レコード - 継承
(record Pos3D Pos (z))
(val p3d (Pos3D :x 1 :y 2 :z 3))
(isinstance p3d Pos)
;=> True
(isinstance p3d Pos3D)
;=> True

;レコード - メソッド(メソッドも継承されます)
(record Pos2 (x y)
  (def print (self)
    (print (str self.x) + ", " + (str self.y))))
(val p2 (Pos2 1 2))
(p2.print)
;-> 1, 2

;data式で関係のあるレコードを共通の親レコードでまとめて定義できます。
(data Pos
  (Pos2D x y)
  (Pos3D x y z))
;上記は下記と同じです。
(record Pos ())
(record Pos2D Pos (x y))
(record Pos3D Pos (x y z))

(data Tree
  (Node left right)
  (Leaf value))
;上記は下記と同じです。
(record Tree ())
(record Node (left right))
(record Leaf (value))

;遅延シーケンス
;イテレータを遅延シーケンスに変換できます。
;遅延シーケンスはイテレータと同様に必要になったときに値を生成しますが、
;一度生成した値をメモリ上に保持するので、
;通常のシーケンス(リスト)と同じように使用することができます。
(val ls (lazyseq (range 10000000000000000000)))
(get ls 10)
;=> 10
(ls 12) ;lazyseqではこの形式でも値を参照できます。
;=> 12
(get ls 0 10) ; スライスを取得する対応する関数はgetsliceです。
;=> (0 1 2 3 4 5 6 7 8 9)
(defseq ls2 (range 100000000000000000))
;=> (val ls2 (lazyseq (range 100000000000000000)))と同じ意味です。
	  

Pythonモジュールの使用

;Flask(http://flask.pocoo.org/)の使用例です。
(from-import flask Flask)
; python の from flask import Flask と同じです。
; (import flask) (val Flask flask.Flask) も同じです。

(val app (Flask "demo"))
; Python 3.4.0 では、Python のバグでエラーになります。

@(app.route "/")
(def hello ()
  "Hello World!")

(app.run)
;ブラウザでhttp://127.0.0.1:5000/を開くと
;Hello World!と表示されます。
	  

制御構文いくつか

;ifはArc()のifと同じです。
(if a b c d e)
;上のif文ではaが真ならb、そうではなくcが真ならd、aもcも真でないならeを評価した値になります。
;(if a b)のように引数が偶数個の形式も書けます。

;doは一つ以上の式を順次評価します。do構文全体の値は、最後の式を評価した値です。
;doはifと組み合わせて使うことを想定しています。
(if True
  (do
   (print "True")
   (+ 1 2))
  (print "False"))
;-> True
;=> 3

;forはpythonのfor文と同様です。
;(forはeachでもOKです。)
;Pythonのfor文と同様に名前空間を新たに作りません。
;for構文全体の評価結果はNoneです。
(for item (range 3)
  (print item))
;-> 0
;-> 1
;-> 2
;=> None


;forの変数部分のみ再代入を許しています。
;下記のような再代入はコンパイル時エラーとなります。
(val value 100)
(for value (range 3)
  (print value))
;=> valueの重複定義エラー
(for value2 (range 3)
  (print value2))
(val value2 100)
;=> vlaue2の重複定義エラー
(for value3 (range 3)
  (val value3 100))
;=> value3の重複定義エラー

;Arcのaccumマクロが使えます。
;http://arclanguage.github.io/ref/list.html#accum
(accum accfn
  (each x '(1 2 3)
    (accfn (* x 10))))
;=> (10 20 30)

;whileは一般的なwhileと同様です。
;while構文全体の評価値はNoneです。
(while True
  (print "繰り返し"))

組み込みのデコレータ

;末尾再帰でスタックを消費しないようにするデコレータ
@recur
(def facti (n a)
    (if (= n 0)
        a
        (facti (- n 1) (* n a))))

(def fact (n)
    (facti n 1))

(fact 10000)
;=> 28242294079...(長いので省略)

;引数リストの左側からカリー化するデコレータ
;(右側からバージョンのrcurriedもあります) 
@curried
(def add_ (x y)
  (+ x y))

(val add100 (add_ 100))

(add100 11)
;=> 111

;partial関数を明示的に呼ばなくても自動的に部分適用するデコレータ
@auto-partial
(def add-n (x n)
  (+ x n))

(val add-100 (add-n 100))

(add-100 11)
;=> 111

;LRUキャッシュでメモ化するデコレータ(Pythonのfunctoolsモジュールにあるデコレータ)
@(lru_cache)
(def prin (n)
  (print n))

(prin 100)
;-> 100
;=> None

(prin 100)
;引数100に対応する結果の値がキャッシュされているため、関数本体が実行されることなくキャッシュされた値(None)が返ります。
;=> None

イテレータを返す関数の例

(map |* _ 10| '(1 2 3))
;=> <map object at 0x102306e90>

(tuple (map |* _ 10| '(1 2 3)))
;=> (10 20 30)

(tuple (mapchain |tuple* _ (* _ 10)| '(1 2 3))))
;=> (1 10 2 20 3 30)

;readlinesはファイルの先頭から行を順番に返すイテレータを返す。
;返ってきたイテレータがイテレートし終わると、自動的にファイルが閉じられます。
(let ((lines (readlines "test.txt")))
  (for line lines
    (print line :end "")))
;-> (text.txtの各行が表示される)
;=> None

そのほかの関数の例とスレッディングマクロ

(reduce |* _1 _2| '(1 2 3 4 5))
;=> 120

; 下記のスレッディングマクロはClojureと同様です(たぶん)。
(-> "1" (+ "23") (+ "4"))
;=> "1234"

(->> "1" (+ "23") (+ "4"))
;=> "4231"

タプル内容表記、ジェネレータ

; タプル内包表記
(tuple-of (* x x) (x (range 10)))
;=> (0 1 4 9 16 25 36 49 64 81)

(tuple-of (* x x) (x (range 10) :when (even x)))
;=> (0 4 16 36 64)

; リスト内包表記
(list-of (* x x) (x (range 10) :when (even x)))
;=> (0 4 16 36 64)

; yieldはPythonと同じです。
(def 10* ()
  (while True
    (yield 10)))

(val g (10*))

(next g)
;=> 10
(next g)
;=> 10

(def test ()
  (yield "test"))

(val t (test))

(next t)
;=> "test"
(next t)
;=> エラー発生

(silent |next t|)
;=> None

(ignore |next t| 100)
;=> 100

(on-err |print (type _)| |next t|)
;-> <class 'StopIteration'>
;=> None

変数への代入もどき

(val v (ref 10))
;=> refオブジェクト

(get! v)
;=> 10

;get!の省略形
#!v
;=> 10

;set!現在値を引数として第2引数の関数を実行し、その値を第1引数のrefオブジェクトに設定します。
(set! v |+ _ 1|)
;=> 11

(get! v)
;=> 11

モジュール

(module my-module (name print-name)
  (val name "my-module")
  (val name2 "my-module")
  (def print-name ()
    (print name)))
(my-module.name)
;=> "my-module"
(my-module.name2)
;=> エラー
(my-module.print-name)
;-> my-module

別スクリプトファイルのロード(コンパイル時)

; ファイルのロード(コンパイル時)
; (load "filepath")
(load "test.purple")

; ファイルのロード(ただし、同一ファイルのロードは一度だけ)
; (require "filepath")
(require "test.purple")
; => test.purpleファイルのロード(コンパイル時)
(require "test.purple")
; => 何もおこらない。

パターンマッチ

; (match <マッチ対象>
;   <パターン> <パターンマッチしたときに実行される式>
;   ...)
; match式は上から順番にパターンにマッチ対象がマッチするかを判別し、
; マッチしていらたら対応する式を実行し、その値を返します。
; いずれのパターンにもマッチしない場合は、Falseを返します。
; 以下に各パターンの例を適当に記載します。
; 各パターンは組み合わせることが可能です。
; たとえば、シーケンスパターンの中にマップパターンなど。

; なんでもマッチするパターン
; _ は任意の値にマッチします
(match '(1 2 3)
  _ "true")
;=> "true"

; シーケンスパターン
; タプル、リスト、文字列、レコードなどにマッチします。
(match '(1 2 3)
  (1 2 3) 100)
;=> 100

(match '(1 2 3)
  (2 3 4) 200
  (1 2 3) 100)
;=> 100

(match '(1 2 3)
  (1 & x) x)
;=> (2 3)

(match '(1 2 3)
  (1 x 3) x)
;=> 2

(record Pos (x y))
(val p (Pos 1 2))
(match p
  (1 v) v)
;=> 2

; レコードパターン
(match '(1 2)
  (Pos 1 2) True)
;=> False

(match (Pos 1 2)
  (Pos 1 y) y)
;=> 2

(match (Pos3D 1 2 3)
  (Pos 1 2 3) True)
;=> True
; レコードの継承関係と構造がマッチするためTrue

; マップ(テーブル)パターン
(match {:x 3 :y 4}
  {:x 3 :y y} y)
;=> 4

; 関数パターン
(match '(1 2 3)
  |= _ '(1 2 3)| 100)
;=> 100

(match '(1 2 3)
  |= _ '(1 2 3)| m1)
;=> (1 2 3)

(match '(1 2 3)
  (|= _ 1| |= _ 2| 3) m2)
;=> 2


; def/match
(def/match test
  (x y z) z
  (x) x)

; 上記は下記と同一
(def test (args)
  (match args
    (x y z) z
    (x) x))

(test 1)
;=> 1

(test 1 2 3)
;=> 3

;defmはdef/matchと同じです。
;以下、defmを使います。

; TODO 型パターンやandパターン、orパターン、fnパターンなどの説明を書く。
(defm plus
  ((int a) (str b)) (+ (str a) b)
  ((str a) (int b)) (+ a (str b))
  ((int a) (int b)) (+ a b)
  _ "error!")
;=> 
(plus 1 2)
;=> 3
(plus 1 "2")
;=> "12"
(plus "1" "2")
;=> "error!"
(plus "1" 2)
;=> "12"

マクロ定義

(mac defseq (name iterable)
  `(val ,name (lazyseq ,iterable)))

; 参考)https://www.hackerschool.com/blog/13-list-comprehensions-in-eight-lines-of-clojure
(mac tuple-of (& form)
 (val (bodyexpr bindingform) form)
 (if (= (len bindingform) 0)
     `(tuple* ,bodyexpr)
   (do
	(val (binding seqexpr & bindings) bindingform)
	(if (= binding ':when)
	    `(if ,seqexpr (tuple-of ,bodyexpr ,bindings))
	  `(mapcat (fn (,binding) (tuple-of ,bodyexpr ,bindings))
		   ,seqexpr)))))