Hy では Python のジェネレータを使用することができます。使い方は Python とほぼ同じです。
=> (defn make-gen [] (yield "foo") (yield "bar") (yield "baz")) => (setv g (make-gen)) => g <generator object make_gen at 0x7fc54655f840> => (for [x g] (print x)) foo bar baz => (lfor x (make-gen) x) ["foo" "bar" "baz"]
=> (defn make-gen1 [] ... (let [a 0 b 0 c 0] ... (setv a (yield "foo")) ... (print a) ... (setv b (yield "bar")) ... (print b) ... (setv c (yield "baz")) ... (print c))) => (setv g (make-gen1)) => (.send g None) "foo" => (.send g 1) 1 "bar" => (.send g 2) 2 "baz" => (.send g 3) 3 Traceback (most recent call last): File "stdin-6194136a591058376d7927257cc1f60b096c3b23", line 1, in <module> (.send g 3) StopIteration
リスト : 入れ子のリストを木と見立てて巡回する (defn foreach-tree [xs] (if (not (is (type xs) list)) (yield xs) (for [x xs] (yield :from (foreach-tree x))))) ; yield-from => yield :from (ver 1.0.0)
=> (setv g (foreach-tree [1 [2 [3] 4] 5])) => (next g) 1 => (next g) 2 => (next g) 3 => (next g) 4 => (next g) 5 => (next g) ... 略 ... StopIteration => (for [x (foreach-tree [1 [2 [3] 4] 5])] (print x)) 1 2 3 4 5 => (lfor x (foreach-tree [1 [2 [3] 4] 5]) x) [1 2 3 4 5]
リスト : エラトステネスの篩 ;;; 整数列 (defn integers [n] (while True (yield n) (setv n (+ n 1)))) ;;; フィルター (defn stream-filter [pred s] (while True (let [x (next s)] (when (pred x) (yield x))))) ;;; エラトステネスの篩 (defn sieve [n nums ps] (if (= n 0) ps (let [x (next nums)] (.append ps x) (sieve (- n 1) (stream-filter (fn [y] (!= (% y x) 0)) nums) ps))))
=> (sieve 25 (integers 2) []) [2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97] => (sieve 100 (integers 2) []) [2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541] => (sieve 500 (integers 2) []) ... 略 ... RecursionError: maximum recursion depth exceeded
リスト : 複数のコルーチンを呼び出す (defn make-coroutine [code] (while True (print code :end "") (yield None))) (defn test1 [n] (let [a (make-coroutine "h") b (make-coroutine "e") c (make-coroutine "y") d (make-coroutine "!") e (make_coroutine " ")] (for [_ (range n)] (for [g [a b c d e]] (next g))) (for [g [a b c d e]] (.close g))))
=> (test1 5) hey! hey! hey! hey! hey! => (test1 10) hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! => (test1 15) hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! =>
リスト : 子コルーチンから子コルーチンを呼び出す (defn make-coroutine2 [code g] (while True (print code :end "") (when g (next g)) (yield None))) (defn test2 [n] (let [e (make-coroutine2 " " None) d (make-coroutine2 "!" e) c (make-coroutine2 "y" d) b (make-coroutine2 "e" c) a (make-coroutine2 "h" b)] (for [_ (range n)] (next a)) (for [g [a b c d e]] (.close g))))
=> (test2 5) hey! hey! hey! hey! hey! => (test2 10) hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! => (test2 15) hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! =>
リスト : 簡単なマルチプロセス ;;; 中断中のプロセスを格納するキュー (setv proc-queue []) ;;; キューに追加 (defn enqueue [x] (.append proc-queue x)) ;;; キューからデータを取り出す (defn dequeue [] (let [x (get proc-queue 0)] (del (get proc-queue 0)) x)) ;;; キューは空か? (defn isempty [] (= (len proc-queue) 0)) ;;; プロセスの生成 (defn fork [f] (let [g (f)] (.send g None) (.append proc-queue g))) ;;; メインプロセス (defn main-process [#* args] ;; プロセスの登録 (for [f args] (fork f)) ;; 実行 (while (not (isempty)) (let [p (dequeue)] (if (.send p False) (enqueue p) (.close p))))) ;;; 子プロセス (defn test3 [name n] (while (> n 0) (print name n) (yield True) (setv n (- n 1))) (yield False))
=> (main-process (fn [] (test3 "foo" 5)) (fn [] (test3 "bar" 7))) foo 5 bar 7 foo 4 bar 6 foo 3 bar 5 foo 2 bar 4 foo 1 bar 3 bar 2 bar 1 => (main-process (fn [] (test3 "foo" 5)) (fn [] (test3 "bar" 7)) (fn [] (test3 "oops" 6))) foo 5 bar 7 oops 6 foo 4 bar 6 oops 5 foo 3 bar 5 oops 4 foo 2 bar 4 oops 3 foo 1 bar 3 oops 2 bar 2 oops 1 bar 1
;;; ;;; sample2.hy : Hylang sample programm ;;; ;;; Copyright (C) 2024-2025 Makoto Hiroi ;;; ;;; 入れ子のリストを木と見立てて巡回する (defn foreach-tree [xs] (if (not (is (type xs) list)) (yield xs) (for [x xs] (yield :from (foreach-tree x))))) ;;; 整数列 (defn integers [n] (while True (yield n) (setv n (+ n 1)))) ;;; フィルター (defn stream-filter [pred s] (while True (let [x (next s)] (when (pred x) (yield x))))) ;;; エラトステネスの篩 (defn sieve [n nums ps] (if (= n 0) ps (let [x (next nums)] (.append ps x) (sieve (- n 1) (stream-filter (fn [y] (!= (% y x) 0)) nums) ps)))) ;;; コルーチン的な使い方 (defn make-coroutine [code] (while True (print code :end "") (yield None))) (defn test1 [n] (let [a (make-coroutine "h") b (make-coroutine "e") c (make-coroutine "y") d (make-coroutine "!") e (make_coroutine " ")] (for [_ (range n)] (for [g [a b c d e]] (next g))) (for [g [a b c d e]] (.close g)))) (defn make-coroutine2 [code g] (while True (print code :end "") (when g (next g)) (yield None))) (defn test2 [n] (let [e (make-coroutine2 " " None) d (make-coroutine2 "!" e) c (make-coroutine2 "y" d) b (make-coroutine2 "e" c) a (make-coroutine2 "h" b)] (for [_ (range n)] (next a)) (for [g [a b c d e]] (.close g)))) ;;; ;;; マルチプロセス ;;; ;;; 中断中のプロセスを格納するキュー (setv proc-queue []) ;;; キューに追加 (defn enqueue [x] (.append proc-queue x)) ;;; キューからデータを取り出す (defn dequeue [] (let [x (get proc-queue 0)] (del (get proc-queue 0)) x)) ;;; キューは空か? (defn isempty [] (= (len proc-queue) 0)) ;;; プロセスの生成 (defn fork [f] (let [g (f)] (.send g None) (.append proc-queue g))) ;;; メインプロセス (defn main-process [#* args] ;; プロセスの登録 (for [f args] (fork f)) ;; 実行 (while (not (isempty)) (let [p (dequeue)] (if (.send p False) (enqueue p) (.close p))))) ;;; 子プロセス (defn test3 [name n] (while (> n 0) (print name n) (yield True) (setv n (- n 1))) (yield False))
Python のモジュール asyncio を import すると、Hy でも非同期 IO (コルーチン) を使用することができます。
(defn :async name [args ...] body ...)
(asyncio.run coro)
(await coro)
リスト : コルーチンの簡単な例 (async_test.hy) (import asyncio) (import time) (defn :async hello [] (print "Hello ...") (await (asyncio.sleep 1)) (print "... World!") True)
=> (import async_test *) => (asyncio.run (hello)) Hello ... ... World! True
(asyncio.sleep delay [result None])
リスト : 複数のコルーチン (1) (defn :async hello2 [] (print "Hello2 ...") (await (asyncio.sleep 2)) (print "... World!") 2) (defn :async hello3 [] (print "Hello3 ...") (await (asyncio.sleep 3)) (print "... World!") 3) (defn :async hello4 [] (print "Hello4 ...") (await (asyncio.sleep 4)) (print "... World!") 4) (defn :async test00 [] (let [s (time.time) a (await (hello4)) b (await (hello3)) c (await (hello2))] (print a b c) (print (- (time.time) s))))
=> (asyncio.run (test00)) Hello4 ... ... World! Hello3 ... ... World! Hello2 ... ... World! 4 3 2 9.009217262268066 =>
(async.create_task coro) => Task object
リスト : 複数のコルーチン (2) (defn :async test01 [] (let [t1 (asyncio.create_task (hello4)) t2 (asyncio.create_task (hello3)) t3 (asyncio.create_task (hello2)) s (time.time) a (await t1) b (await t2) c (await t3)] (print a b c) (print (- (time.time) s))))
=> (asyncio.run (test01)) Hello4 ... Hello3 ... Hello2 ... ... World! ... World! ... World! 4 3 2 4.0026795864105225
(asyncio.gather coro ...) => [result ...]
リスト : 複数のコルーチン (3) (defn :async test02 [] (let [s (time.time) r (await (asyncio.gather (hello4) (hello3) (hello2)))] (print r) (print (- (time.time) s))))
=> (asyncio.run (test02)) Hello4 ... Hello3 ... Hello2 ... ... World! ... World! ... World! [4, 3, 2] 4.002763986587524
リスト : 複数のコルーチン (4) (defn :async test-sub [name n] (while (> n 0) (print name n) (await (asyncio.sleep 0)) (setv n (- n 1))) False) (defn :async test03 [] (let [r (await (asyncio.gather (test-sub "foo" 5) (test-sub "bar" 3) (test-sub "baz" 7)))] (print r)))
=> (asyncio.run (test03)) foo 5 bar 3 baz 7 foo 4 bar 2 baz 6 foo 3 bar 1 baz 5 foo 2 baz 4 foo 1 baz 3 baz 2 baz 1 [False, False, False]
リスト : キューの使用例 (defn :async send-color [color n queue] (while (> n 0) (await (.put queue color)) (await (asyncio.sleep 0)) (setv n (- n 1)))) (defn :async receive-color [n queue] (while (> n 0) (let [item (await (.get queue))] (print item :end " ")) (setv n (- n 1)))) (defn :async test04 [] (let [q (asyncio.Queue 4)] (await (asyncio.gather (send-color "red" 9 q) (send-color "blue" 7 q) (send-color "yellow" 8 q) (receive-color 24 q)))))
=> (asyncio.run (test04)) red blue yellow red blue yellow red blue yellow red blue yellow red blue yellow red blue yellow red blue yellow red yellow red [None None None None]
リスト : 哲学者の食事 ;;; フォークの初期化 (defn init-forks [] (global forks) (setv forks (lfor _ (range 5) True))) ;;; フォークの番号を求める (defn fork-index [person side] (if (= side "right") person (% (+ person 1) 5))) ;;; フォークがあるか (defn isFork [person side] (get forks (fork_index person side))) ;;; フォークを取る (defn :async get-fork [person side] (while True (when (isFork person side) (setv (get forks (fork_index person side)) False) (break)) (await (asyncio.sleep 1))) (await (asyncio.sleep 1)) True) ;;; フォークを置く (defn :async put-fork [person side] (setv (get forks (fork-index person side)) True) (await (asyncio.sleep 1)) True) ;;; 哲学者の動作 (デッドロック) (defn :async person0 [n] (for [_ (range 2)] (print f"Philosopher {n} is thinking") (await (get-fork n "right")) (await (get-fork n "left")) (print f"Philosopher {n} is eating") (await (asyncio.sleep 1)) (await (put-fork n "right")) (await (put_fork n "left"))) (print f"Philosopher {n} is sleeping")) (defn :async test05 [] (init_forks) (let [ps (lfor n (range 5) (person0 n))] (await (asyncio.gather #* ps))))
=> (asyncio.run (test05)) Philosopher 0 is thinking Philosopher 1 is thinking Philosopher 2 is thinking Philosopher 3 is thinking Philosopher 4 is thinking ^C KeyboardInterrupt
リスト : デッドロックの解消 (defn :async person1 [n] (for [_ (range 2)] (print f"Philosopher {n} is thinking") (if (= (% n 2) 0) (do (await (get-fork n "right")) (await (get-fork n "left"))) (do (await (get-fork n "left")) (await (get-fork n "right")))) (print f"Philosopher {n} is eating") (await (asyncio.sleep 1)) (await (put-fork n "right")) (await (put_fork n "left"))) (print f"Philosopher {n} is sleeping")) (defn :async test06 [] (init_forks) (let [ps (lfor n (range 5) (person1 n))] (await (asyncio.gather #* ps))))
=> (asyncio.run (test06)) Philosopher 0 is thinking Philosopher 1 is thinking Philosopher 2 is thinking Philosopher 3 is thinking Philosopher 4 is thinking Philosopher 0 is eating Philosopher 3 is eating Philosopher 0 is thinking Philosopher 1 is eating Philosopher 3 is thinking Philosopher 0 is eating Philosopher 1 is thinking Philosopher 2 is eating Philosopher 4 is eating Philosopher 0 is sleeping Philosopher 2 is thinking Philosopher 1 is eating Philosopher 4 is thinking Philosopher 3 is eating Philosopher 1 is sleeping Philosopher 2 is eating Philosopher 3 is sleeping Philosopher 4 is eating Philosopher 2 is sleeping Philosopher 4 is sleeping [None None None None None]
;;; ;;; sample3.hy : Hylang sample programm ;;; ;;; Copyright (C) 2024-2025 Makoto Hiroi ;;; (import asyncio) (import time) ;;; ;;; コルーチン ;;; (defn :async hello [] (print "Hello ...") (await (asyncio.sleep 1)) (print "... World!") True) (defn :async hello2 [] (print "Hello2 ...") (await (asyncio.sleep 2)) (print "... World!") 2) (defn :async hello3 [] (print "Hello3 ...") (await (asyncio.sleep 3)) (print "... World!") 3) (defn :async hello4 [] (print "Hello4 ...") (await (asyncio.sleep 4)) (print "... World!") 4) (defn :async test00 [] (let [s (time.time) a (await (hello4)) b (await (hello3)) c (await (hello2))] (print a b c) (print (- (time.time) s)))) (defn :async test01 [] (let [t1 (asyncio.create_task (hello4)) t2 (asyncio.create_task (hello3)) t3 (asyncio.create_task (hello2)) s (time.time) a (await t1) b (await t2) c (await t3)] (print a b c) (print (- (time.time) s)))) (defn :async test02 [] (let [s (time.time) r (await (asyncio.gather (hello4) (hello3) (hello2)))] (print r) (print (- (time.time) s)))) (defn :async test-sub [name n] (while (> n 0) (print name n) (await (asyncio.sleep 0)) (setv n (- n 1))) False) (defn :async test03 [] (let [r (await (asyncio.gather (test-sub "foo" 5) (test-sub "bar" 3) (test-sub "baz" 7)))] (print r))) ;;; キューの使用例 (defn :async send-color [color n queue] (while (> n 0) (await (.put queue color)) (await (asyncio.sleep 0)) (setv n (- n 1)))) (defn :async receive-color [n queue] (while (> n 0) (let [item (await (.get queue))] (print item :end " ")) (setv n (- n 1)))) (defn :async test04 [] (let [q (asyncio.Queue 4)] (await (asyncio.gather (send-color "red" 9 q) (send-color "blue" 7 q) (send-color "yellow" 8 q) (receive-color 24 q))))) ;;; ;;; 哲学者の食事 ;;; ;;; フォークの初期化 (defn init-forks [] (global forks) (setv forks (lfor _ (range 5) True))) ;;; フォークの番号を求める (defn fork-index [person side] (if (= side "right") person (% (+ person 1) 5))) ;;; フォークがあるか (defn isFork [person side] (get forks (fork_index person side))) ;;; フォークを取る (defn :async get-fork [person side] (while True (when (isFork person side) (setv (get forks (fork_index person side)) False) (break)) (await (asyncio.sleep 1))) (await (asyncio.sleep 1)) True) ;;; フォークを置く (defn :async put-fork [person side] (setv (get forks (fork-index person side)) True) (await (asyncio.sleep 1)) True) ;;; 哲学者の動作 (デッドロック) (defn :async person0 [n] (for [_ (range 2)] (print f"Philosopher {n} is thinking") (await (get-fork n "right")) (await (get-fork n "left")) (print f"Philosopher {n} is eating") (await (asyncio.sleep 1)) (await (put-fork n "right")) (await (put_fork n "left"))) (print f"Philosopher {n} is sleeping")) (defn :async test05 [] (init_forks) (let [ps (lfor n (range 5) (person0 n))] (await (asyncio.gather #* ps)))) ;;; デッドロックの解消 (defn :async person1 [n] (for [_ (range 2)] (print f"Philosopher {n} is thinking") (if (= (% n 2) 0) (do (await (get-fork n "right")) (await (get-fork n "left"))) (do (await (get-fork n "left")) (await (get-fork n "right")))) (print f"Philosopher {n} is eating") (await (asyncio.sleep 1)) (await (put-fork n "right")) (await (put_fork n "left"))) (print f"Philosopher {n} is sleeping")) (defn :async test06 [] (init_forks) (let [ps (lfor n (range 5) (person1 n))] (await (asyncio.gather #* ps))))
(for [:async item agen] ...)
(await (.asend agen value)) => item
リスト : 非同期ジェネレータの簡単な使用例 (import asyncio) ;;; 非同期ジェネレータ (defn :async async-gen [] (let [a 0 b 0 c 0] (setv a (yield "foo")) (print a) (await (asyncio.sleep 0.5)) (setv b (yield "bar")) (print b) (await (asyncio.sleep 0.5)) (setv c (yield "baz")) (print c))) (defn :async test0 [] (let [xs []] (for [:async x (async-gen)] (.append xs x)) xs)) (defn :async test1 [] (lfor :async x (async-gen) x)) (defn :async test2 [] (let [a 0 b 0 c 0 d 0 g (async-gen)] (try (setv a (await (.asend g None))) (print a) (setv b (await (.asend g 1))) (print b) (setv c (await (.asend g 2))) (print c) (setv d (await (.asend g 3))) (print d) (except [StopAsyncIteration] None)) [a b c])) (defn :async test3 [] (let [a 0 b 0 c 0 d 0 g (async-gen)] (try (setv a (await (.__anext__ g))) (print a) (setv b (await (.__anext__ g))) (print b) (setv c (await (.__anext__ g))) (print c) (setv d (await (.__anext__ g))) (print d) (except [StopAsyncIteration] None)) [a b c]))
=> (asyncio.run (test0)) None None None ["foo" "bar" "baz"] => (asyncio.run (test1)) None None None ["foo" "bar" "baz"] => (asyncio.run (test2)) foo 1 bar 2 baz 3 ["foo" "bar" "baz"] => (asyncio.run (test3)) foo None bar None baz None ["foo" "bar" "baz"]
一般的なプログラミング言語の場合、関数を呼び出す前に引数が評価され、その結果が関数に渡されます。これを「正格 (strict) な評価」といいます。これに対し、引数や変数の値が必要になるまで評価を行わない方法もあります。具体的には、引数や変数を参照するときに評価が行われます。これを「遅延評価 (delayed evaluation または lazy evaluation)」といいます。
プログラミング言語では純粋な関数型言語である Haskell が遅延評価です。また、Scheme でも delay と force を使って遅延評価を行うことができます。そして、その評価結果は保存されることに注意してください。再度変数や引数を参照すると、保存されている値が返されます。
なお、値の保存 (キャッシング) をしないでよければ、クロージャを使って遅延評価を行うこともできます。Hy (Python) はクロージャとマクロをサポートしているので、遅延評価を実装することは簡単です。今回は Hy で Scheme ライクな遅延評価を作ってみましょう。
遅延評価の詳しい説明は拙作のページをお読みください。
リスト : 遅延評価 ;;; プロミスの定義 (defclass Promise [] (defn __init__ [self expr] (setv self.thunk expr self.result None)) ;; プロミスの評価 (defn force [self] (when self.thunk (setv self.result (self.thunk) self.flag True self.thunk None)) self.result)) ;;; プロミスの生成 (defmacro delay [expr] `(Promise (fn [] ~expr)))
delay はマクロで、引数 expr を評価しないでクラス Promise のオブジェクトを生成して返します。Scheme ではこれを「プロミス」といいます。本ページでもプロミスと呼ぶことにしましょう。expr はラムダ式に包んで Promise の変数 thunk (サンク) にセットします。
メソッド force は Promise に格納された関数を評価して、その結果を返します。変数 thunk が真の場合、関数 thunk はまだ評価されていません。thunk を評価して、その返り値を変数 result にセットします。それから、thunk の値を None に書き換えます。thunk が偽ならば既に評価されているので何もしません。最後に result を返します。
簡単な使用例を示しましょう。
=> (import lazy *) => (require lazy *) => (setv a (delay (do (print "oops") (+ 1 2)))) => a <lazy.Promise object at 0x7f496dcec250> => (.force a) oops 3 => (.force a) 3
(delay (do ...)) の返り値を変数 a にセットします。このとき、S 式 (do ...) は評価されていません。プロミスの値を実際に求めるメソッドが force です。(.force a) を評価すると、S 式 (do ...) を評価して値 30 を返します。このとき、画面に oops が表示されます。次に、(.force a) を評価すると、式を評価せずにキャッシュした値を返すので oops は表示されません。
もう一つ、簡単な使用例を示しましょう。皆さんお馴染みの「たらいまわし関数」は、遅延評価で高速化することができます。次のリストを見てください。
リスト : たらいまわし関数 (defn tarai [x y z] (if (<= x y) y (tarai (tarai (- x 1) y z) (tarai (- y 1) z x) (tarai (- z 1) x y)))) ;;; 遅延評価による高速化 (defn tarai-l [x y z] (if (<= x y) y (tarai-l (tarai-l (- x 1) y z) (tarai-l (- y 1) (.force z) (delay x)) (delay (tarai-l (- (.force z) 1) x (delay y))))))
関数 tarai のプログラムを見てください。x <= y のときに y を返しますが、このとき引数 z の値は必要ありませんね。引数 z の値は x > y のときに計算するようにすれば、無駄な計算を省略することができます。tarai の引数 z を遅延評価した関数が tarai-l です。引数 z にデータを渡すとき、delay でプロミスを生成します。そして、その値を取り出すときは (.force z) とします。これで遅延評価を行うことができます。
実行結果を示します。
=> (import time) => (do (setv s (time.time)) (print (tarai 12 6 0)) (- (time.time) s)) 12 1.208184003829956 => (do (setv s (time.time)) (print (tarai-l 12 6 (delay 0))) (- (time.time) s)) 12 0.00016951560974121094 実行環境 : Ubunts 22.04 (WSL2), Intel Core i5-6200U 2.30GHz
delay と force で tarai を高速に実行することができました。遅延評価の効果は十分に出ていると思います。