9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Lisperのためのhylang

Last updated at Posted at 2022-01-22

他のLisp系言語をそれなりに知っている人がHyについて簡単に知るためのメモ。随時更新。

Overview

  • シンタックスはClojure風。ただしプログラミングスタイルはPythonに近い
  • ClojureやSchemeと同様に変数と関数で名前空間が分かれていない(Lisp-1)
  • Common LispやClojureに近いスタイルのマクロが使える(いわゆる伝統的マクロ)
  • Pythonを呼び出したり、逆にPythonから呼び出すこともでき、Hyのコードから対応するPythonのコードを生成することもできる
  • 1.0に向けて破壊的変更が激しくなっている。より生のPythonに近いシンタックスに寄せてきている印象
    • 一時期1.0のalpha版ということで1.0a4といったバージョンになっていたがそれ以降は0.24.0に戻され、現在は0.27.0が最新になっている

関数

関数適用

他のLispと同様、関数をリストの最初に置くと関数適用となる。
リストの2要素目以降(引数)が左から右へ順に評価されていき、全ての値が出揃ったらリスト先頭の関数に適用される。

(+ 1 2 3) ; => 6

Common Lispのように関数と変数で名前空間が分かれていないので、値として渡ってきた関数を適用するときもfuncallなどは不要で、単純にリストの先頭に置くだけである。(Scheme、Clojureと同じLisp-1。Pythonも分類上はLisp-1)

(defn add-n-generator [n]
  (fn [x] (+ x n)))

((add-n-generator 10) 5) ; => 15

apply 改め #*

多くのLispには、引数として関数とリストを受け取り、リストを展開して与えられた関数に適用させるためにapplyが用意されている。
かつてはHyにもapplyがあったが、今は#*リーダーマクロに置き換えられている。リストの前に#*をつけることがでそのリストが展開される。

(+ #*'(1 2 3)) ; => 6
(+ #*[1 2 3])  ; => 6

;; #*を並べて書くこともできる
(+ #*[1 2 3] #*[4 5]) ; => 15

Hy 0.27.0 から更に破壊的変更が入り、リーダーマクロをシンボル前につける場合、シンボルとの間にスペースが必要になった。

(setv lst [1 2 3])
;; (+ #*lst) ; => error (hy > 0.27.0)
(+ #* lst) ; => 6

同様に、辞書型のデータを展開してキーワード引数として与えることもできる。それには #** を使う。

(defn keyword-arg-func [#** kwargs]
  (print "kwargs: " kwargs))

(keyword-arg-func :key1 1 :key2 2)
; kwargs:  {'key1': 1, 'key2': 2}

(keyword-arg-func #**{"key1" 1, "key2" 2})
; kwargs:  {'key1': 1, 'key2': 2}

関数定義

(defn fact [n]
  "docstring for fact"
  (if (= n 0)
    1
    (* n (fact (- n 1)))))

helpでdocstringを参照できる。

(help fact)

; fact(n)
;     docstring for fact

ラムダリストパラメータ

この辺はHy 1.0で大幅に変わる可能性がある。

&rest

+のように可変長引数を取るときに使う。&restで指定した局所変数には可変長引数として渡されたものが入り、関数内からはリストとして参照できる。

(defn my-plus [&rest args]
  (+ #* args))

(my-plus 1 2 3) ; => 6

&optional

&optionalパラメータはオプショナル引数とキーワード引数の複合のような指定の仕方になっている。
キーワードを指定して呼び出すことも指定しないで呼び出すこともできる。デフォルト値を指定しないとNoneになる。

(defn optional-arg [&optional key1 [key2 42] key3]
  [key1 key2 key3])

(optional-arg)                     ; => [None 42 None]
(optional-arg 1 2 3)               ; => [1 2 3]
(optional-arg 'key1)               ; => ['key1', 42]
(optional-arg :key2 420)           ; => [None 420 None]
(optional-arg :key2 420 :key3 123) ; => [None 420 123]

&kwargs

&kwargsパラメータは形式を定めずにキーワード引数として受け付け、関数内からはディクショナリ型として参照できる。

(defn kwargs-test [&kwargs kwargs]
  kwargs)

(kwargs-test :key1 "val1" :key2 "val2") ; => {"key1" "val1"  "key2" "val2"}
(type (kwargs-test :key1 "val1" :key2 "val2")) ; => <class 'dict'>

Hy 1.0からの変更点

アルファ版(Hy 1.0a4)の段階では、&rest, &optional, &kwargsはいずれも無くなり、オプショナル引数はデフォルト値が必須になり [optional1 None]のように指定することになる。可変長引数は#*で指定することになる。mata,任意のキーワード引数は#**で指定するようになる。
呼び出し側では従来通りオプショナル引数はキーワード引数のようにも呼び出せる、ただしキーワード引数のように呼び出すとrestパラメータは指定できなくなる。

(defn f [required1 required2 [optional1 None] [optional2 "foo"] #* rest #** kwargs]
  (setv formtxt "required1: {0}, required2: {1}, optional1: {2}, optional2: {3}, rest: {4}, kwargs: {5}")
  (print (formtxt.format required1 required2 optional1 optional2 rest kwargs)))

(f 1 2 3 4 5 6 7 8 :nine 9 :ten 10)
;; required1: 1, required2: 2, optional1: 3, optional2: 4, rest: (5, 6, 7, 8), kwargs: {'nine': 9, 'ten': 10}

(setv lst [5 6 7 8])
(f 1 2 3 4 #* lst)
;; required1: 1, required2: 2, optional1: 3, optional2: 4, rest: (5, 6, 7, 8), kwargs: {}

(setv dic {"nine" 9, "ten" 10})
(f 1 2 3 4 #* lst #** dic)
;; required1: 1, required2: 2, optional1: 3, optional2: 4, rest: (5, 6, 7, 8), kwargs: {'nine': 9, 'ten': 10}

無名関数

lambdaではなく、ClojureやArcと同様にfnを使う。本体部分が1つの式を返すだけだとPythonのlambdaに変換されるが、複数の式から構成されていると_hy_anon_var_XXなどの名前付き関数に変換される。

(fn [x] (+ x 1)) ; => <function <lambda> at 0x7fbd9062e8b0>

((fn [x] (+ x 1)) 10) ; => 11

Lisp-1なので、次のように無名関数を変数に代入する関数定義もできる。

(setv add
      (fn [x y]
        (+ x y)))

(add 2 3) ; => 5

map, reduce, filterなどの高階関数群

map, reduce, filter などの高階関数は期待通りに使える。mapfilterなどの並びを返す関数はその段階では要素が評価されずに遅延シーケンスオブジェクトとして返ってくる。これらはlistなどによってはじめて実現化される。

(defn inc [n] (+ n 1))
(list (map inc (range 1 10))) ; => [2, 3, 4, 5, 6, 7, 8, 9, 10]
(reduce + (range 1 11)) ; => 55
(list (filter even? (range 1 10))) ; => [2, 4, 6, 8]

普通にPythonのitertoolsも使える。例えば、reduceの溜め込みの途中経過をリストとして取得したければaccumulateが使える。

(import [itertools :as iter])
(list (iter.accumulate (range 1 11) +)) ; => [1 3 6 10 15 21 28 36 45 55]

標準のletとマクロによるletの定義

letは標準ではcontribパッケージとしてHyに同梱されている。スタイルとしてはClojureのletと同じ局所変数と値の対応がフラットに並ぶタイプのletである。個人的には変数と値の対応が分かりにくくなるので苦手だが、Clojureと同様に,を入れて見やすくすることもできる。
展開形を見ると無名関数の冒頭で変数に代入しているだけであり、Common Lispのlet*のように前で局所的に定義した変数をその後の変数定義の中で使える。
importrequireについては後述するが、マクロを利用するためにはhy.contrib.walkモジュールからletマクロをrequireする必要がある。

(require [hy.contrib.walk [let]])
(let [x 1
      y (+ x 1)]
  (print (.format "x: {0}, y: {1}" x y)))
; x: 1, y: 2

マクロの導入にもなるので、同等のものを自分で定義してみよう。

(defmacro let* [var-pairs &rest body]
  `((fn []
      (setv ~@var-pairs)
      ~@body)))

(let* [one 1
       two (+ one one)
       [three four] [(+ one two) (+ two two)]] ; 展開形を見ると分配束縛もできることが分かる
  (print one two three four))
; 1 2 3 4

マクロ定義はCommon Lispに近い(伝統的マクロとか呼ばれたりする)。準クォート(バッククォート)内で式の評価するためのアンクォートは~、リストを展開するためのスプライシングアンクォートは~@で、ここはClojureと同じだ。
マクロの展開にはmacroexpand-1が使える。

(macroexpand-1
  '(let* [one 1
          two (+ one one)
          [three four] [(+ one two) (+ two two)]] ; 展開形を見ると分配束縛もできることが分かる
     (print one two three four)))

;; '((fn []
;;     (setv one 1
;;           two (+ one one)
;;           [three four] [(+ one two) (+ two two)])
;;     (print one two three four)))

Hy 1.0からのlet

Hy 1.0a4からletはコアパッケージに取り込まれている。
Pythonへの展開形を見るとレキシカルスコープを作るというよりはユニークな変数を作っているだけのようである。

(let [x 10
      y (+ x 20)]
  (print "x:" x ", y:" y))

;; _hy_let_x_23 = 10
;; _hy_let_y_24 = _hy_let_x_23 + 20
;; print('x:', _hy_let_x_23, ', y:', _hy_let_y_24)

制御構造

条件分岐

if

ifは他のLispとほぼ同じだが、真理値の扱いはPythonと同じ癖があり、色々なものが偽となりうるので注意が必要。

次のものが全て偽として扱われる。

  • False
  • None
  • 0, 0.0
  • 空リスト[]
  • 空tuple#()
  • 空dict{}
  • 空のHyリスト'()
  • 空文字 ""
(if (or False None 0 0.0 [] #() {} '() "")
    "真" "偽")
;; => "偽"

Hy 0.24.0で破壊的変更があり、ifは偽の式が必須となった。3引数でないときはエラーになる
条件が真の場合のみ評価したい場合はwhenを使う。whenは条件に合わないときはNoneを返す。

(if True "This is true") ; => Error

(when True "This is true")

do

doは処理をブロックにまとめる構文で、内部の式を順次評価していき、最後の式の値をdoブロック全体の値として返す。レキシカルスコープは作らない。
Clojureのdo, Common Lispのprogn, Schemeのbeginに相当する。

(do (print "foo")
    (print "bar")
    10)
; foo
; bar
; => 10

cond

condは条件部と本体のペアを[]で囲む。デフォルト節の条件部は:elseとしているが、真になるものであれば何でもいい。
Hy 0.24.0で破壊的変更が入り、条件部と本体のペアを[]で囲まないようになった(Clojureと同じ)。複数の式を並べたいときは本体部分にdoを入れる必要がある

(defn fib [n]
  (cond [(= n 0) 0]
        [(= n 1) 1]
        [:else (+ (fib (- n 1))
                  (fib (- n 2)))]))

;; Hy 0.24.0 以降
(defn fib [n]
  (cond (= n 0) 0
        (= n 1) 1
        :else (+ (fib (- n 1))
                 (fib (- n 2)))))

(list (map fib (range 1 11))) ; => [1 1 2 3 5 8 13 21 34 55]

when / unless

when / unless はifdoの組合せで、条件部が真(偽)のときに本体部分を順に評価していき最後の式の値を返す。

Hy 1.0a4から破壊的変更が入り、unlessはHyruleという別パッケージに分離された

(when True
  (print "one")
  (print "two")
  3)
; one
; two
; => 3

;; Hy 1.0a4以降はhyruleからrequireする必要がある
(require hyrule [unless])

(unless False
  (print "one")
  (print "two")
  3)
; one
; two
; => 3

マクロで定義するとこうなる。

(defmacro my-when [predicate &rest body]
  `(if ~predicate
       (do ~@body)))

(my-when True
  (print "one")
  (print "two")
  3)
; one
; two
; => 3

繰り返し

シーケンスに対する繰り返し: for

(for [x ["a" "b" "c"]]
  (print x))

; a
; b
; c

(for [x (range 0 10)]
  (print x))

;; 0
;; 1
;; 2
;; 3
;; 4
;; 5
;; 6
;; 7
;; 8
;; 9

2つのループ変数を並べると二重ループになる。これならばforを二重にする方が意図が明確になる分まだいい気がする。

(for [i [1 2 3]
      j [3 2 1]]
  (print (.format "i: {0}, j: {1}" i j)))

; i: 1, j: 3
; i: 1, j: 2
; i: 1, j: 1
; i: 2, j: 3
; i: 2, j: 2
; i: 2, j: 1
; i: 3, j: 3
; i: 3, j: 2
; i: 3, j: 1

(for [i [1 2 3]]
  (for [j [3 2 1]]
    (print (.format "i: {0}, j: {1}" i j))))

Common Lispのloopマクロのforのように複数のシーケンスを並行して舐めていくには、zip を使うなどして複数のシーケンスの各要素のペアからなる1つのシーケンスとしてforを回す。
forのループ変数にも分配束縛のような書き方ができる。

(for [[i j] (zip [1 2 3] [3 2 1])]
  (print (.format "i: {0}, j: {1}" i j)))

; i: 1, j: 3
; i: 2, j: 2
; i: 3, j: 1

loop/recur

Pythonは末尾再帰最適化を行なわないので、Clojureのloop/recur構文と同じものがHyにもある。再帰呼び出しの関数名がrecurに固定されいると考えればSchemeのnamed-letにも似ている。
これもHyに同梱はされているがロードが必要なマクロとなっている。

(require [hy.contrib.loop [loop recur]])
;; (require hyrule [loop]) ; Hy 1.0a4からユーティリティ群がコアパッケージから分離されている

(defn fast-fib [n]
  (loop [[cnt 0]
         [pre1 1]
         [pre2 0]]
    (if (= cnt n)
        pre2
        (recur (+ cnt 1) (+ pre1 pre2) pre1))))

(fast-fib 100) ; => 354224848179261915075

リスト内包表記: lfor

繰り返しの結果をリストで取得したいときはPythonのリスト内包表記に展開されるlforを使う。
なおmapでも同じことはできる。

(lfor i (range 1 11) (fast-fib i))
;; => [1 1 2 3 5 8 13 21 34 55]

(list (map fast-fib (range 1 11)))
;; => [1 1 2 3 5 8 13 21 34 55]

データ構造

HyExpression: Hyのコードを構成しているリスト

見た目上は他のLispのリストに近しく、クォートで評価から保護しない限り評価規則に従ってプログラムとして評価される。
Pythonのリストとは別のデータ構造として定義されており、データとしてリストを扱うときはPythonのリストを使うのが素直。
Hyのコードはこのデータ型から成り立っており、マクロでの操作対象のデータとなる。

(type '(1 2 3))
;; => <class 'hy.models.HyExpression'>

リスト

Pythonの配列。
要素へのアクセスにはgetを使う。(get list1 0)list1[0]のようにPythonの [] リテラルに展開されるので、dictやtupleについても同じように使えることが分かる。

(type [1 2 3])
;; => <class 'list'>

(setv list1 [1 2 3])
(get list1 0)
;; => 1

getに複数の引数を渡すと入れ子になっているリストなどから値を取り出せる。

(setv mat [[1 2 3]
           [4 5 6]
           [7 8 9]])

(get mat 2 2) ; => 9
(get (get mat 2) 2) ; => 9

リストの特定の要素に代入するには setv と getを組み合わせる。

(setv (get list1 0) 10)
;; Python: list1[0] = 10

list1
;; => [10 2 3]

スライス

スライスは最大3引数の cut に対応する。
https://qiita.com/yossyyossy/items/0c8dc2ed53466d970fe4

(let [from 1
      to None
      by 2]
  (cut [1 2 3 4 5] from to by)) ; => [2, 4]

(cut [1 2 3 4 5] 2 None) ; => [3, 4, 5]
(cut [1 2 3 4 5] 0 2) ; => [1, 2]
(cut [1 2 3 4 5] 2 4) ; => [3, 4]
(cut [1 2 3 4 5] -4 -2) ; => [2, 3]

タプル

イミュータブルなリスト。
(, 1 2 3) のようなリテラルで表記する。,という関数があるわけではないので注意。
Hy 0.24.0から破壊的変更があり、 #(1 2 3) のような表記に変更になった。
イミュータブルなので要素に値を代入しようとするとエラーになる。

(type #(1 2 3)
;; => <class 'tuple'>

(setv tuple1 #(1 2 3))
(setv (get tuple1 0) 10)

; Traceback (most recent call last):
;   File "stdin-cb58b269f1fa70dc13ef1b1eaa148b4fca2878af", line 1, in <module>
;     (setv (get tuple1 0) 10)
; TypeError: 'tuple' object does not support item assignment

set (集合型)

集合型。順序を持たず、要素の重複を許さない。

(type #{1 2 3})
;; <class 'set'>

(dir #{1 2 3})
; [... "add" "clear" "copy" "difference" "difference_update" "discard" "intersection" "intersection_update" "isdisjoint" "issubset" "issuperset" "pop" "remove" "symmetric_difference" "symmetric_difference_update" "union" "update"]

(set [1 2 3])
;; #{1 2 3}

(setv sets #{"a" "b" "c" "d"})
(print sets)
;; {'c', 'd', 'b', 'a'}

(sets.add "e")
(print sets)
;; {'c', 'e', 'b', 'a', 'd'}

(sets.remove "a")
(print sets)
;; {'c', 'e', 'b', 'd'}

(sets.pop)
;; => "c"
(print sets)
;; {'e', 'b', 'd'}

要素が集合に属するかどうかは in 演算子を使う。

(in 1 #{1 2 3}) ; => True

集合演算

;; 包含関係 (<, <=, >, >=)
(<= #{1 2} #{1 2 3}) ; => True
(<= #{1 2 3} #{1 2}) ; => False

;; 同値
(= #{1 2} #{1 2}) ; => True

(setv sets2 #{1 2 3})

;; 積集合
(sets2.intersection #{3 4 5}) ; => #{3}

;; 和集合
(sets2.union #{3 4 5}) ; => #{1 2 3 4 5}

;; 差集合
(sets2.difference #{3 4 5}) ; => #{1 2}
(- sets2 #{3 4 5}) ; => #{1 2}

dict (辞書型)

属性リスト的な使い方で、keyとvalueを交互に並べる。
参照にはリストと同じで get を使い、 setv と組み合わせて代入できる。これは assoc でも同じことができる。

(setv d {"dog" "bark" "cat" "meow"})
(type {"dog" "bark" "cat" "meow"}) ; <type 'dict'>

(d.get "dog") ; => "bark"
(get d "dog") ; => "bark"

(setv (get d "dog") "wan"
      (get d "cat") "nyan")
d ; => {"dog" "wan"  "cat" "nyan"}

(require hyrule [assoc]) ;; Hy 1.0a4 からHyruleに分離され、requireが必要になった
(assoc d "dog" "bark") ; => None
d ; => {"dog" "bark"  "cat" "nyan"}

;; 辞書型に対するループ

(for [[k v] (d.items)]
  (setv formtxt "key: {0}, value: {1}")
  (print (formtxt.format k v)))

;; key: dog, value: bark
;; key: cat, value: nyan

前述の通り、関数のキーワード引数に展開して与えることもできる。

(defn say [[dog "bark"] [cat "meow"]]
  (print "Dog say" dog)
  (print "Cat say" cat))

(say #** d)

;; Dog say bark
;; Cat say nyan

番外編:numpyのarray

(import numpy :as np)

(setv arr (np.array [[1 2 3]
                     [4 5 6]
                     [7 8 9]]))
arr.flags
;; C_CONTIGUOUS : True
;; F_CONTIGUOUS : False
;; OWNDATA : True
;; WRITEABLE : True
;; ALIGNED : True
;; WRITEBACKIFCOPY : False
;; UPDATEIFCOPY : False

arr.ndim ; => 2
arr.size ; => 9
arr.dtype ; => dtype('int64')

;; スカラー倍
(* arr 3)
;; array([[ 3,  6,  9],
;;        [12, 15, 18],
;;        [21, 24, 27]])

;; アダマール積(要素積)
(* arr arr)
;; array([[ 1,  4,  9],
;;        [16, 25, 36],
;;        [49, 64, 81]])

;; 行列積
(np.dot arr arr)
;; array([[ 30,  36,  42],
;;        [ 66,  81,  96],
;;        [102, 126, 150]])

;; 一様乱数で100x100行列を作って行列積を取る
(import numpy.random :as rand)

(setv bigarr1 (rand.rand 1000 1000))
(setv bigarr2 (rand.rand 1000 1000))

(np.dot bigarr1 bigarr2)

;; array([[ 28.38096367,  28.63420504,  28.01482173, ...,  27.26330009,
;;          25.56717227,  27.39401733],
;;        [ 25.26386499,  23.78039895,  22.81641922, ...,  24.37012314,
;;          22.31017675,  22.20606049],
;;        [ 24.79624411,  23.11758526,  24.45533016, ...,  24.47093385,
;;          22.3951194 ,  24.02735416],
;;        ..., 
;;        [ 25.65465691,  25.7403632 ,  23.54518075, ...,  24.36247407,
;;          21.92434498,  23.04834359],
;;        [ 22.37135022,  21.32717967,  21.92101116, ...,  20.93922527,
;;          20.07961519,  20.54109093],
;;        [ 27.50945536,  25.99902791,  25.73058543, ...,  25.71283456,
;;          23.86456424,  25.27311888]])
9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?