Scheme:初心者の質問箱

Scheme:初心者の質問箱

メーリングリストで質問したり、WiLiKiに自分のページを作ったりするのはちょっと… というシャイなあなたのためのスペースです。

あたらしい質問は、項目の先頭に追加していって下さい。

書き方を間違えても小人さんが直してくれるので、 こわがらなくてもだいじょうぶ。

長くなってきたので過去ログ:



オブジェクトシステムで next-method がない場合のエラーに関して

オブジェクトシステムにおいて呼び出し候補のメソッドで一番最後のメソッドから(next-method)を呼んだ場合に ERROR: no applicable method for #<generic xyz-method (5)> のエラーが発生します。 仕様だとは思うのですが、このエラーが発生しないようにする方法や、次のメソッドが存在するかどうかを調べる方法 (next-method-exists?)のような関数、 あるいは、no applicable method エラーのみを補足するような方法ありますでしょうか。

(guard (e [else 'no-next-method]) (next-method)) としたのですが当然、次のメソッドでエラーが起きた場合に必要なエラー処理ができなくなってしまい不都合が生じてしまいました。

クラスとメソッドをセットで生成するマクロを書いていて、継承関係にあるメソッドを順に呼びたいのですが、上記のエラーに対応する方法があれば、マクロを呼び出す段階で、next-methodを呼ぶかどうか指定しなくてよくなるのですが。

displayタイプの表示のpprint

(display '("a" "b"))は(a b)と表示され、(write '("a" "b"))は("a" "b")と表示されます。

(pprint '("a" "b"))は("a" "b")と表示されるので、writeと同じタイプの方の表示です。

displayと同じタイプの表示でプリティプリントをするにはどうすればいいでしょうか?

tls の使用

Gauche で curl の様なスクリプトを書こうと思い rfc.http モジュールを使って

(use rfc.http)
(receive (status headers content) 
        (http-post "httpbin.org" "/post" '(("id" "123") ("name" "foo")) )
                (print content))

とすると

{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "id": "123", 
    "name": "foo"
  }, 
...

と返ってくるのですが、https://httpbin.org に API を叩くにはどうすればいいか分からずにいます。

rfc.tls モジュールを使えばいいらしいということは分かったのですが、mbedtls をどうやって組み込めばいいか分かりませんので

./configure --with-tls=mbedtls-internal

としてビルドし直しました。最初の2、3回は

gosh> (make-tls)
#<mbed-tls #f (unconnected)>

と返ってきましたが、その後

Segmentation fault (コアダンプ)

で gosh が止まってしまいます。どうも mbedtls が上手く組み込めないようです。

rfc.tls のページや Gauche release notes を読むと、どうも mbedtls が前提のようですが、その他の方法(例えば cert と 秘密鍵を直接指定して)で tls で接続することは出来ないのでしょうか?

ちなみに

gosh tools/get-cacert

は、やってみました。

(use rfc.http)
(receive (status headers content) 
    (http-post "httpbin.org" "/post" '(("id" "123") ("name" "foo")) :secure #t)
  (print content))
;
; ==> {
;       "args": {},
;       "data": "",
;       "files": {},
;       "form": {
;         "id": "123",
;         "name": "foo"
;       },
;         :
;         :
;       "url": "https://httpbin.org/post"
;     }

hash-table の変更

こんにちは。

「ハッシュテーブルって連想リストみたいなものでしょ? じゃあ連想リストのような二重リストでよいのでは?」

と適当に

(make-hash) => '()

(hash-table-get ht key) => (car (assoc-ref リスト キー))

などとやってみましたが、hash-table-put! で躓きました。

(define (hash-table-put! ht key val)
  (set! ht (cons (list key val) ht)))

こうしても ht へ破壊的代入が出来ません。何となく、関数は適用するもので対象を変えるものではないのだろうから、関数内で set! してもそこを出たら元のままなのだろう、というのは納得出来るのですが(違うかもしれませんが)、では一方、何故ハッシュテーブルは hash-table-put! で変更可能なのか分かりません。「それがハッシュテーブルだから」ということなのかもしれませんが…… よろしくお願いします。

(define (make-pseudo-hash-table) (list '()))
(define (pseudo-hash-table-put! ht key val)
  (set-car! ht (cons (list key val) (car ht)))
  ;(set-car! ht (assoc-adjoin (car ht) key (list val)))
  ;(set-car! ht (assoc-set! (car ht) key (list val)))
  )
(define (pseudo-hash-table-get  ht key)
  (car (assoc-ref (car ht) key)))

(define ht1 (make-pseudo-hash-table))
(pseudo-hash-table-put! ht1 'key1 1000)
(pseudo-hash-table-put! ht1 'key2 2000)
(pseudo-hash-table-put! ht1 'key3 3000)
(pseudo-hash-table-put! ht1 'key1 10000)
(print (pseudo-hash-table-get ht1 'key1)) ; ==> 10000
(print (pseudo-hash-table-get ht1 'key2)) ; ==> 2000
(print (pseudo-hash-table-get ht1 'key3)) ; ==> 3000

d (describe) を当てて見ると set! は an instance of class <syntax> で set-car! は an instance of class <procedure> なのですね。(ちょっと吃驚)

なるほど、macro

(define-macro (hash-table-put! ht key val)
  `(set! ,ht (cons (list ,key val) ,ht)))

にすると何とかなりました。 いつもありがとうございます。

get-gauche.sh でのテストエラーに関して

Linux環境(Fedora 40)でget-gauche.sh を使って以下のようにインストールをしようとしています。

> ./get-gauche.sh --version 0.9.15 --prefix /path/to/be/installed

configureからコンパイルはうまくいくのですが、test で以下のようなエラーがでます。

Testing ffitest ...                                              failed.
9 discrepancies found:
test load-foreign: expects (#t (f_i () i) (f_i_i (i) i)) => got #<<error> ": no matching clause for (.struct _IO_cookie_io_functions_t () ((read (.pointer () (.type cookie_read_function_t () (.function () (.type __ssize_t () (long ())) ((__cookie (.pointer () (void ()))) (__buf (.pointer () (char ()))) (__nbytes (.type size_t () (u-long ())))))))) (write (.pointer () (.type cookie_write_function_t () (.function () (.type __ssize_t () (long ())) ((__cookie (.pointer () (void ()))) (__buf (.pointer () (char (const)))) (__nbytes (.type size_t () (u-long ())))))))) (seek (.pointer () (.type cookie_seek_function_t () (.function () (int ()) ((__cookie (.pointer () (void ()))) (__pos (.pointer () (.type __off64_t () (long ())))) (__w (int ()))))))) (close (.pointer () (.type cookie_close_function_t () (.function () (int ()) ((__cookie (.pointer () (void ()))))))))))">
test call-foreign f_c: expects 9 => got #<<error> "flib is supposed to be of type #<class <foreign-library>>, but got #f">
test call-foreign f_i: expects 42 => got #<<error> "flib is supposed to be of type #<class <foreign-library>>, but got #f">
test call-foreign f_f: expects 1.25 => got #<<error> "flib is supposed to be of type #<class <foreign-library>>, but got #f">
test call-foreign f_d: expects 3.14 => got #<<error> "flib is supposed to be of type #<class <foreign-library>>, but got #f">
test call-foreign f_v: expects #<undef> => got #<<error> "flib is supposed to be of type #<class <foreign-library>>, but got #f">
test call-foreign f_i_i: expects 99 => got #<<error> "flib is supposed to be of type #<class <foreign-library>>, but got #f">
test call-foreign f_f_f: expects 0.125 => got #<<error> "flib is supposed to be of type #<class <foreign-library>>, but got #f">
test call-foreign f_d_d: expects 1.0 => got #<<error> "flib is supposed to be of type #<class <foreign-library>>, but got #f">

なお、ffitest をコンパイルする箇所は以下のように問題ないようです。 環境の問題でしょうか。 よろしくお願いします。

"../../src/gosh" -ftest "../../lib/tools/precomp" -e -P -o gauche--ffitest ./ffitest.scm
mkdir -p test
gcc -DHAVE_CONFIG_H -I. -I. -I../../src -I../../src -I../../gc/include    -g -O2 -Wall -Wextra -Wno-unused-label -fPIC  -fomit-frame-pointer  -c ./test/f.c
gcc -g -O2 -Wall -Wextra -Wno-unused-label -fPIC     -shared -o test/f.so f.o
gcc -DHAVE_CONFIG_H -I. -I. -I../../src -I../../src -I../../gc/include    -g -O2 -Wall -Wextra -Wno-unused-label -fPIC  -fomit-frame-pointer  -o gauche--ffitest.o -c gauche--ffitest.c
gcc -g -O2 -Wall -Wextra -Wno-unused-label -fPIC     -shared -o gauche--ffitest.so gauche--ffitest.o -L"../../src"   -lmbedtls -lcrypt -lrt -lm  -lpthread
"../../src/gosh" -ftest "../../ext/xlink" -l -g "gauche" \
  -b "../.." -s . gauche--ffitest.so ffitest.sci
gcc -DHAVE_CONFIG_H -I. -I. -I../../src -I../../src -I../../gc/include    -g -O2 -Wall -Wextra -Wno-unused-label -fPIC  -fomit-frame-pointer  -o text--line-edit.o -c text--line-edit.c
link /home/******/Gauche/build-20240621_213915.8uqq6wZA/Gauche-0.9.15/ext/native/gauche--ffitest.so <- ../../src/gauche--ffitest.so
link /home/******/Gauche/build-20240621_213915.8uqq6wZA/Gauche-0.9.15/ext/native/ffitest.sci <- ../../lib/gauche/ffitest.sci

Windowsにおけるtools/build-standaloneについて

前回質問したものです。おかげさまで0.9.15でLinuxとMacでは無事スタンドアロンの実行ファイルを作ることが出来ました。しかし、Windowsではld.exeが-liconvと-lzを見つけられないと言うエラーがが出てしまいます。

前回の時点では良く判っていなかったのですが、Windows版でスタンドアロン実行ファイルを作るのに必要なminGWは、自分はWindows版のnim2.0をインストールしていたので条件をクリアしていたようです。

c:/nim-2.0.0/dist/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.1.0/../../../../x86_64-w64-mingw32/bin/ld.exe: cannot find -liconv
c:/nim-2.0.0/dist/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/11.1.0/../../../../x86_64-w64-mingw32/bin/ld.exe: cannot find -lz
collect2.exe: error: ld returned 1 exit status

nimでインストールされたminGW環境に不足があるのか、Gaucheのディレクトリに適切なpathが通っていないか、どちらかの原因ではないかと思うのですが、どうすれば良いでしょうか?

tools/build-standaloneについて

ドキュメントに従ってスタンドアロン実行可能ファイルを作ろうとしたのですが、Linux、Windows、Mac全てで以下の様なアラートが出て失敗してしまいます。 gaucheのインストールはインストールスクリプト、またはWin版インストーラで行いバージョンは0.9.14です。 スクリプトの内容はhello worldなので、それ由来のエラーではないと思います。 アラートを見るとconfigP.hファイルが不足しているようなのですが、どうすれば良いでしょうか?

/usr/local/lib/gauche-0.98/0.9.14/include/gauche.h:45:10: fatal error: 'gauche/priv/configP.h' file not found
#include "gauche/priv/configP.h" /* Only used while compiling Gauche itself */
         ^~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

\0 区切りの文字列の read

find(1) の -print0 で出力されるような \0 で区切られた文字列を、read-line が \n で区切られた文字列を読むような形で簡単に読み込む方法はありますでしょうか。現在自作の関数で一応読めてはいるのですが、もし簡単な方法があればと思い質問させていただきました。

Shiro(2024/02/24 12:47:16 UTC): 全部読み込んで文字列にしてから #\x00 でsplitするのではなく、 関数を呼ぶ度に次の#\x00まで読みたい、ということでしょうか。一発でやるのは多分ありませんが、 ジェネレータを使うのが楽かなと思います。

質問者(2024/02/24 17:29:30 UTC): 了解いたしました。
ご回答ありがとうございました。

対話的に起動した際、使用する文字コードを指定することは可能でしょうか

Shiro (2024/02/23 09:37:46 UTC): 今は一般的な仕組みはないです。Windows consoleで使う場合に consoleのコードページを見て何かするコードはあるんですが、ハードコードされてます。 もう今はutf-8決め打ちでいいんじゃないかと思ってるんですが、 いや指定したいんだというユースケースがあれば教えてください。

対話的に起動した際、C-r で履歴をインクリメンタルに検索する設定は可能でしょうか

Shiro (2024/02/23 09:37:46 UTC): ああ、欲しいですね。今はないです。 https://github.com/shirok/Gauche/issues/989 でトラックします。

R7RSモードでの起動時の-lで指定したロードファイルの挙動について

以下のようなgauche1.scm, gauche2.scm のファイルを用意したディレクトリで、

;; gauche1.scm
(import (scheme base))
(load "./gauche2.scm")
;; gauche2.scm
(display "Hello Gauche")

gosh -r 7 -l ./gauche1.scm

とR7RSモードでGaucheを開始しようとすると、 gosh: "UNBOUND-VARIABLE-ERROR": unbound variable: load とエラーがでます。

load関数自体は、普通にGaucheから利用できるのですが、lオプションで指定したロードファイルは何か特別な配慮が必要なのでしょうか。よろしくお願いします。

SRFI-49で開きカッコがネストしている場合の書き方

SRFI-49では、

(quote (1 (2 (3 4))))

を、カッコを使わずに

quote
  1
    2
      3 4

と書けるというのはわかりましたが、

(quote (((1 2) 3) 4))

(quote (((1))))

を、カッコを使わずに書く場合はどう書くのですか?

連想リスト(Association list)の形式について

連想リストを作るときに、リストのリストにするのと、ドットペアのリストにするのとどちらが良いとか、どちらが普通などありますでしょうか? 例えば、Schemeの関数との相性がどちらが良いかなど。

'((1 "A") (2 "B") (3 "C")) ;; 形式1
'((1 . "A") (2 . "B") (3 . "C")) ;; 形式2

また、値の方に、複数の値の組をもたせたいような場合も、以下の形式のどちらがお勧めなどありますでしょうか。assoc したあとに、cdr で値のリストが得られる形式3の方が使い勝手良さそうにも思いますが、ペアという見た目では形式4の方が良いようにも思います。assoc の後にset-cdr! するときも、形式3の方が自然かなとおもいましたが、どうでしょうか。

'((1 "AA" "AB" "AC") (2 "BB" "BC" "BD") (3 "CC" "CD" "CE")) ;; 形式3
'((1 ("AA" "AB" "AC")) (2 ("BB" "BC" "BD")) (3 ("CC" "CD" "CE"))) ;; 形式4

ケースバイケースだと思いますが、Scheme初心者ということもあり、どちらでもよい場合に、どちらを使っておくのがおすすめか教えて頂けますと幸いです。

バックグラウンドで動かすにはどうすれば良いでしょうか

例えばシェルスクリプト hello.sh

#!/bin/bash

while :; do                     # 5秒ごとに hello, と標準出力させる
        echo "Hello,"
        sleep 5
done

と world.sh

#!/bin/bash

self=$$ # このスクリプトの PID

bash hello.sh & # バックグラウンドで hello.sh を動かす(表示させる)
hello=$! # hello.sh が動いている PID

read line
if [[ $line == "change" ]]; then # change を標準入力したら、
        kill $hello # hello.sh を PID ごと止め、

        while :; do # world を3秒ごとに標準出力させる
                echo "world!"
                sleep 3
        done
else                    # change 以外を標準入力したら全部止める
        kill $hello
        kill $self
fi

を書いて bash world.sh とすると hello, hello, ... (5秒以内に change と打ち込む) world! world! ...

と出来ます。これを scheme(Gauche)でやろうと試みました。gosh は一つのプロセスとして動いているので、そのプロセス内のスレッドを使い分けるしかないのかもと思い gauche.threads モジュールを使って thread.scm

(use gauche.threads)
(define *hello*)
(define (hello-thread)
  (let ((t (make-thread
            (lambda ()
              (while #t
                     (sys-sleep 5)
                     (print "Hello, "))))))
    (set! *hello* t)
    (thread-start! t)))

(while (if (not (equal? (read) 'change))
           (hello-thread)
           (begin
             (thread-terminate! *hello*)
             (while #t
                    (sys-sleep 3)
                    (print "world")))))

と書き

gosh thread.scm

で同様のことが出来ました。(最初に何か入力しなければいけませんが)

しかし gauche.threads のページに

「単に同時実行が必要なだけなら、call/ccによる協調スレッド テクニックが使えるでしょう」

とあります。call/cc のジャンプ(脱出)が使えるのかと思い

(define (hello)
  (while #t
         (sys-sleep 5)
         (print "Hello,")))

(begin
  (call/cc
   (lambda (break)
     (while #t
            (if (not (equal? (read) 'change))
                (hello)
                (break "Change!")))))
  (while #t
         (sys-sleep 3)
         (print "world")))

と書いて実行してみましたが当然、hello 自身のループから出て入力受け付けに辿り着くことなく、change を入力してもずっと hello, を出力し続けました。 これは

1. コードが悪い。実は call/cc でも可能

2. バックグラウンドで動かすには gauche.threads を使って別にスレッドを分ける必要がある

3. その他(delay force あたりで出来る気もします)

のどれかなのでしょうか。

Typo報告

GithubにIssueだすほどではないように思いましたので、ここに記載させていただきました。

srfi.227のドキュメントでopt-lambda が opt-labmda になっていました。 Macro: opt-labmda opt-formals body … Macro: opt*-labmda opt-formals body …

delete! の作用について

こんにちは。gosh -V => (version "0.9.9") を使用しております。

(define abc '(a b c))
(delete! 'a abc) ;=> abc (a b c)
(delete! 'b abc) ;=> abc (a c)

となるのですが、前者で abc が (b c) にならないのは何故でしょうか。

(remove! (^(elt) (equal? elt 'a)) abc)
(remove! (^(elt) (equal? elt 'b)) abc)

でも同様に前者は abc の値が変わりません。よろしくお願いします。

(define abc (list 'a 'b 'c))
(set! abc (delete! 'a abc))
(print abc) ; ==> (b c)

(define abc (list 'a 'b 'c))
(set! abc (delete! 'b abc))
(print abc) ; ==> (a c)

(define abc (list 'a 'b 'c))
(set! abc (remove! (^(elt) (equal? elt 'a)) abc))
(print abc) ; ==> (b c)

(define abc (list 'a 'b 'c))
(set! abc (remove! (^(elt) (equal? elt 'b)) abc))
(print abc) ; ==> (a c)

shutdown 後に socket-status が返す値

突然お邪魔して,申し訳ありません.いつも楽しく Gauche を利用させていただいております.初心者です.

#!/usr/bin/env gosh
(use gauche.net)
(letrec ((socket (make-server-socket 'inet '8080))
         (client (socket-accept socket)))
  (socket-shutdown client)
  (format #t "~A\n" (socket-status client)))

としましたところ,

shutdonw

と表示されましたが,これは「shutdown」のタイポでしょうか?それとも何か理由があるのでしょうか?Gaucheのバージョンは0.9.10です.場違いな書き込みでしたら,すみません.

while 内の唐突な代入について

累積関数 factorial

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

を while で書き直すことを考えると

(define (w-fac n)
  (let ((b 1))
    (while
     [> n 1]
     (set!-values (n b) (values (- n 1) (* n b))))
    b))

こんな感じになると思います。(末尾再帰化と似ている) これを、段階を細分化し

0: b に1を代入し

1: n が1か否かで処理を分け

2: n=1 でない場合は b:=n*b n:=n-1 として処理を続け

3: n=1 の場合は b を返す

という while でさらに書き換えます。 (u の値がおよそ上記の番号に当たる)

(define (w-fac2 n)
  (let ((u 0) (b 0))
    (while
     [< u 3]
     (cond ((= u 0)
            (set!-values (u n b) (values 1 n 1)))
           ((= u 1)
            (set!-values (u n b) (values (if (= n 1) 3 2) n b)))
           ((= u 2)
            (set!-values (u n b) (values 1 (- n 1) (* n b))))))
    b))

そしてこれからが本題なのですが、while に入る前に u だけでなく b の値も決めておかなければ実行時に「bが未定義シンボルだ」とエラーが出ます。(当たり前といえば当たり前ですが) while 前に代入(ではなく束縛ですが)するのは状態を判断して処理を振り分ける u だけで出来ないかと色々試してみました。

しかし、例えば u=0 の場合に

(define b 0)

としてみると「トップレベル以外で define は無理」とエラーになります。また、関数の引数も最初のもの(factorial)が1つなので、b をあらかじめ与えられるようにすると「同じ関数を while で書き換える」とは違うように思います。

Python などで

def w_fac2 (n) :
    u = 0
    while u < 3 :
        if u == 0 :
            u, n, b = 1, n, 1
        elif u == 1 :
            u, n, b = (lambda a: 3 if a == 1 else 2)(n), n, b
        else :
            u, n, b = 1, n -1, n * b
    return b

と、いきなり b に代入するようなことは、Scheme(Gauche) では無理なのでしょうか。 何となく出来たら出来たで恐ろしい気もしますが(バグが紛れ込みそう)、出来ないことを明確に知りたいと思います。 よろしくお願いします。

Gauche の、Racket との違いと評価順序について

一つ前の質問をした者です。齊藤さん、hamayamaさん、Shiroさん、丁寧なお答えを頂きありがとうございました。

call/cc について調べているとこの様なページを見つけました。 https://stackoverflow.com/questions/52807955/infinite-loop-while-using-call-with-current-continuation-in-scheme[

ちょっといじって

(define (f n)
  (let ((p (call/cc identity)))
    (sleep 1) ; 無限ループで分からなくならないように
    (display n)
    p))

((f #\newline) (f "*"))

とすると call/cc パズルhttps://practical-scheme.net/wiliki/wiliki.cgi?Scheme%3A%E4%BD%BF%E3%81%84%E3%81%9F%E3%81%84%E4%BA%BA%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E7%B6%99%E7%B6%9A%E5%85%A5%E9%96%80#H-2x6wuk[ と同じように、DrRacket では動きました。 が、Gauche で

(define (f n)
  (let ((p (call/cc identity)))
    (sys-sleep 1) ; 無限ループで分からなくならないように
    (display n)
    p))

((f #\newline) (f "*"))

とすると * -> 改行 -> 改行 -> * 改行 -> 改行 -> * …… となります。 何故かを考えますと、まず Racket ではおそらく

簡略のため、並列された上から順序で評価される式を -> で書いています

([let ((p (call/cc identity))) 改行->p] [let ((p (call/cc identity))) *->p]) ; ①
([let ((p k1)) 改行->p] [let ((p (call/cc identity))) *->p]) ; ② 継続 k1 が生成される(identity を引数に取っているため call/cc が返す値は継続そのもの)
改行 & (k1 [let ((p (call/cc identity))) *->p] ;③ k2 が生成される
(k1 [let ((p k2)) *->p]) ; ④
* & (k1 k2) ; ⑤
([let ((p k2)) 改行->p] [let ((p (call/cc identity))) *->p]) ; ⑥ k1 が生成された時点の ② の k1 に k2 が当て嵌められる
改行 & (k2 [let ((p k3)) *->p]) ; ⑦ k3 が生成される
* & (k2 k3) ; ⑧
(k1 [let ((p k3)) *->p]) ; ⑧
* & (k1 k3) ; ⑨
...

と、② 式(ただし kn の n の値は変わっていく)に戻る度に遡るためのステップが一つずつ増えていくことで * が改行前に一つずつ増えて表示されるのだと思います。 対して Gauche では最初に * が表示されることから被適用される前の式より、適用する後ろの式から評価されるのだと推測しました。すると

([let ((p (call/cc identity))) 改行->p] [let ((p (call/cc identity))) *->p]) ; ①
([let ((p (call/cc identity))) 改行->p] [let ((p k1)) *->p]) ; ② k1 が生成される
* & ([let ((p (call/cc identity))) 改行->p] k1) ; ③ 
([let ((p k2)) 改行->p] k1) ; ④ k2 が生成される
改行 & (k2 k1) ; ⑤ 
([let ((p k1)) 改行->p] k1) ; ⑥ k2 が生成された時点の④式の k2 に k1 が当て嵌められる
改行 & (k1 k1) ; ⑦ 
([let ((p (call/cc identity))) 改行->p] [let ((p k1)) *->p]) ; ⑧ k1 が生成された時点の②式の k1 に k1 が当て嵌められる
* & ([let ((p k3)) 改行->p] k1) ; ⑨ k3 が生成される
(k3 k1) ; ⑩ 
([let ((p k1)) 改行->p] k1) ; ⑪ ⑨式の k3 に k1 が当て嵌められる
改行 & (k1 k1) ; ⑫  k1 が生成された時点の②式の k1 に k1 が当て嵌められる

...

と、②式に戻りそこからの流れが繰り返され(ただし k2 -> k3 -> ...) 改行 -> 改行 -> * ... が繰り返されるのだと推測しました。 しかしこれには間違いがあります。

(define (f n) ...

と f を定義せずに

([let ((p (call/cc identity))) 改行->p] [let ((p (call/cc identity))) *->p])

と直接 let 式で行うと  * -> 改行 * -> 改行 * -> 改行 * -> 改行 ... となってしまうのです。おかしい、何が違うんだ。同じことをやってるんじゃないのかよ……と紙に書いて色々考えた結果、一つの仮定にたどり着きました。

([let ((p (call/cc identity))) 改行->p] [let ((p (call/cc identity))) *->p]) ; ①
([let ((p (call/cc identity))) 改行->p] [let ((p k1)) *->p]) ; ② k1 が生成される
([let ((p k2)) 改行->p] [let ((p k1)) *->p]) ; ③ そしてここで適用する let 式が評価されてしまう前に、被適用の let 式の評価が行われる
* & ([let ((p k2)) 改行->p] k1) ; ④ 
改行 & (k2 k1) ; ⑤ 
([let ((p k1)) 改行->p] [let ((p k1)) *->p]) ; ⑥ ③式に k1 が当て嵌められる
* & ([let ((p k1)) 改行->p] k1) ; ⑦ 
改行 & (k1 k1) ; ⑧ 
([let ((p (call/cc identity))) 改行->p] [let ((p k1)) *->p]) ; ⑨ ②式に k1 が当て嵌められる

...

と、③式から * と改行の出力が行われて②式へ戻ることを繰り返しているのではないかという仮定です。

何故

(define (f n) ...

とすると適用する側の評価が行われる前に被適用側の評価が行われないか考えてみると、おそらく適用側の (f 引数) が明確に値を返すまで評価されるからだと思います。

これらが正しいとすると

・Gauche では適用側(引数)の方から評価が行われる

・let 式は let が束縛する変数が分かると評価は割と後回しにされる

・定義された関数(変数)に束縛された式は値を返すまで評価される

だと思います。しかし call/cc パズルの解説と同じページの「評価順序と継続」 https://practical-scheme.net/wiliki/wiliki.cgi?Scheme%3A%E4%BD%BF%E3%81%84%E3%81%9F%E3%81%84%E4%BA%BA%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E7%B6%99%E7%B6%9A%E5%85%A5%E9%96%80#H-1wby0gx[ によると

「つまり、Gaucheでは左から順に引数を評価するために、」 とあって大前提の

・適用側(引数)の方から評価が行われる

は、間違いのようです。 と、すると Gauche と Racket でこの違いは一体何によるものでしょうか。

ブロック内で call/cc とその他を並列にしたときの継続の中身

begin 内で call/cc 式と他の式を並べてみました。

(define aaa/cc)
(begin
  (call/cc (lambda (k) (set! aaa/cc k)))
  (print "aaa"))

この begin 式を評価すると

gosh> aaa
#<undef>

となります。また

gosh> (aaa/cc)
aaa
#<undef>

というのも(一体どういう仕組みでこれが可能になっているのかはともかく)おそらく call/cc 式の次の (print 'aaa) が aaa/cc に set! で代入されているのだろうというのは推測できます。どういう仕組みでそうなるのかはともかく。 しかし

gosh> (aaa/cc 1)
aaa
#<undef>

gosh> (aaa/cc 'a 'b 'c)
aaa
#<undef>

と、aaa/cc に引数を(幾らでも)渡しても同じように同じ結果を返します。継続は結局はクロージャ、あるいはクロージャの連鎖だと理解しているのですが、aaa/cc に代入された継続は一体どのようなものなのでしょうか。

よろしくお願いします。

;; (A)
(define k1 #f)
(begin
  (call/cc (lambda (k) (set! k1 k)))
  (print "aaa"))
(k1 1)
;; (B)
(define k1 #f)
(print (call/cc (lambda (k) (set! k1 k) 0)))
(k1 1)
;; (C)
(define k1 #f)
(list 100 (call/cc (lambda (k) (set! k1 k) 0)) 200 300)
(k1 1)
;; (D)
(define k1 #f)
(call-with-values (lambda () (call/cc (lambda (k) (set! k1 k) 0))) list)
(k1 1 2 3)

Shiro(2022/10/23 21:03:51 UTC): aaa/cc に渡した値は、(call/cc (lambda (k)... の戻り値になります。デバッグスタブ #?= を使って表示させてみましょう。

(define aaa/cc #f)
(define (foo)
  #?=(call/cc (lambda (k) (set! aaa/cc k)))
  (print "aaa"))

最初の呼び出し:

gosh> (foo)
#?=(call/cc (lambda (k) (set! aaa/cc k)))
#?-    #<subr "continuation">
aaa
#<undef>

ここで表示されているのは、(set! aaa/cc k) の戻り値 (kの値、つまり継続手続き) がそのまま出てきています。

gosh> (aaa/cc 1)
#?-    1
aaa
#<undef>

継続に1を渡してみました。すると、(call/cc ... から1が返ってきて、foo の残りが再び実行されます。

gosh> (aaa/cc 'a 'b 'c)
#?-    a
#?+    b
#?+    c
aaa
#<undef>

三つの値を渡すと、それが多値として(call/cc ... から返ってきます。

なお、トップレベル式で捕まえる継続はR7RSでは明確に定義されておらず、処理系によって振る舞いが分かれるので、私の例のように関数の中で捕まえる方がわかりやすいです。


Last modified : 2024/11/27 12:14:38 UTC