素数夜曲とプログラミング B.4.8-B.4.9 Scheme, Python, Haskell, JavaScript 第 4 回

この記事は10分で読めます

中高の数学の復習から専門的な数学・物理までいろいろな情報を発信しています.
中高数学に関しては中高数学駆け込み寺,
大学数学に関しては現代数学観光ツアーという無料の通信講座があります.
ご興味のある方はぜひお気軽にご登録ください!


素数夜曲とこれまでの内容

素数夜曲

これまでの内容

B.4.8 内部と外部

未定義の文字列 scope を使って次のように入力するとクロージャになるらしい.


(lambda () scope)

そして括弧をつけて評価してみると未定義を警告するエラーが出る.


((lambda () scope))

RESULT

An error occurred.

いったん次のようにトップレベル (クロージャの外部) で定義する.


(define scope 'external)
(write ((lambda () scope)))

RESULT

external

これはクロージャの変数が scope の外部定義された情報を参照していることを表す.
次にクロージャ内部に scopelet で定義してみる.


(write ((lambda ()
         (let ((scope 'internal))
           scope))))

RESULT

internal

もちろん内部で定義した scope を使っている.
これを隠蔽 (shadow) と呼ぶそうだ.

変数の視野

クロージャ内部の let を入れ子にしてさらに隠蔽の仕組みを調べる.


(write ((lambda ()
   (let ((scope 'outer))
     (let ((scope 'inner))
       scope)))))

RESULT

inner

最後の scope に近い内側の let の定義が優先されている.

let で lambda 項との束縛関係も定義できる.


(let ((times2 (lambda (x) (* x 2)))
      (times3 (lambda (x) (* x 3))))
  (write (times3 (times2 2))))

RESULT

12

これは define による定義と似てはいるが times2, times3 は大域的な関数として定義されない.
次の形式では束縛する引数がない.


(let ()
  (define times2 (lambda (x) (* x 2)))
  (define times3 (lambda (x) (* x 3)))
  (write (times3 (times2 2)))
  )

RESULT

12

let の仕様を忘れたので次の確認コードを.
let の外では times2, times3 は未定義だ.


(let ()
  (define times2 (lambda (x) (* x 2)))
  (define times3 (lambda (x) (* x 3)))
  )
(write (times3 (times2 2)))

RESULT

An error occurred.

これに対するのは begin: begin の外でも times2times3 は定義されている.


(begin (define times2 (lambda (x) (* x 2)))
       (define times3 (lambda (x) (* x 3)))
       (write (times3 (times2 2))))
(newline)
(write (times3 (times2 2)))

RESULT

12
12

let*

let の複数の引数設定での順序の問題を考える.
例えば次のコードはエラーになる.
z を設定するときに x, y が設定済みとは限らないから.


(let ((x 2) (y 3) (z (* x y)))
  z)

RESULT

An error occurred.

let を入れ子にする方法もあるが左から右へ順序を限定して設定する let* もある.
まずは入れ子の let を見てみよう.


(let ((x 2) (y 3))
  (let ((z (* x y)))
    (write z)))

RESULT

6

次に let*.


(let* ((x 2) (y 3) (z (* x y)))
  (write z))

RESULT

6

letrec

let の使い分けシリーズ.
次のような形式でも使える.


(write ((letrec ((z (lambda () (* x y))) (x 2) (y 3))
  z)))

RESULT

6

単独では未定義な (* x y)z の値にするから,
単に (z (* x y)) とするとエラーになる (そういう実装もあるしエラーにならない実装もある).
letrec は定数部分からも引数が見えるので再帰関数を定義するのに使える.
define による定義に一番近い形式になるとのこと.

再帰関数の定義に関して名前からすればそうなのだろうが,
少なくとも B パートから読みはじめた限りでは本で実例が出てきていない.
こんな文章だけでは理解できないから P.443 から例を取ってくる.

まずは define による形式から.


(define fact
  (lambda (n)
    (if (zero? n)
        1
        (* n (fact (- n 1))))))
(write (fact 4))

RESULT

24

次に letrec.


(letrec ((fact
          (lambda (n)
            (if (zero? n)
                1
                (* n (fact (- n 1))) ))))
  (write (fact 5)))

RESULT

120

書いてみたのはいいが letrec でないといけないところはどこだろう.
letrecletlet* にするとエラーになることは確認した.

B.4.9 set!

既に定義されている変数の値を変更するためには set! を使う.
! は破壊的な変更を表すのに使う.
この流儀, 少なくとも Ruby は受け継いでいる.
他にも何かあった気がするがパッと思い浮かばない.


(set! [name] [new-value])

[name] には値を変更する変数の名前を,
[new-value] には新たな値を入れる.

試してみよう.
まずは同じ変数相手に define を重ねるとエラーになる.


(define a 3)
(define a 4)

RESULT

An error occurred.

では set! を.


(define a 3)
(write a)
(newline)
(set! a 4)
(write a)

RESULT

3
4

let の内部でも同じ.


(let ((z 0))
  (write z)
  (newline)
  (set! z 2)
  (write z))

RESULT

0
2

大域変数

z をトップレベルで定義すると大域変数になる.
Schem では大域変数をアスタリスクで囲む慣習があるようだ.
しかし短い変数名の変数についてはいちいち *z* のようにはしないらしい.

何はともあれ次の評価を見てみよう.


(define z 5)
(write ((lambda ()
          (let ((z 0))
            (set! z (* 2 3))
            (write z)
            (newline))
          z)))

RESULT

6
5

let の内部で set! をしていて, そこでの評価は確かに 6 になっている.
つまり破壊的に変更されている.
しかし let の外にその変更の情報は出ていかないから lambda の評価値は 5 になる.

このように内部定義に束縛されていない変数は外部に定義を探しに行く.
それはクロージャの窓口として機能する.

しかしこの「それはクロージャの窓口として機能する」というのが何を意味しているのかまだよくわかっていない.
とりあえず本にしたがってコードを書いていこう.


(define z 5)
((lambda ()
   (set! z (* 3 5))))
(write z)

RESULT

15

確かに z が破壊的に変更に変更されていることを示している.


(define z 5)
(write ((lambda ()
          (set! z (* 3 5))
          z)))
(newline)
(write z)

RESULT

15
15

こちらは lambdaz の値そのものを返している.

P.438 の冒頭は lambda の中で lambda の最後に書いた変数が
lambda の返り値になっていると言っているだけで,
それ以上の何かを言っているわけではないと思うがどうなのだろう.

Ruby や Scala はそういう仕様だったと思う.
次のコードは Ruby のコード.


def mytest
  a = 3
end

def mytest2
  a = 4
  a
end

p mytest
p mytest2

RESULT

3
4

特に関数 mytest2 を見てほしい.
最後に a と書いただけで return a と同じ意味になっている.

対比のために Python のコードを.


def mytest():
    a = 3
    a

def mytest2():
    a = 4
    return a

print(mytest())
print(mytest2())

RESULT

None
4

Python はきちんと return しないと値が返らない.

コメントで何度もご指摘頂いたようにクロージャやスコープが全くわかっていないのだろう.
この辺の話すっきりしないものがずっと残っている.
しばらくやっているとそのうちわかるときが来るだろうから,
それまで耐えよう.

大域環境によるカウンタ

大域変数の値を set! で変えると変更された値をまた大域環境から取り出せる.


(define n 0)
(write n)
(newline)
(set! n (+ n 1))
(write n)
(newline)
(set! n (+ n 1))
(write n)
(newline)
(set! n (+ n 1))
(write n)
(newline)

RESULT

0
1
2
3

次の count 関数を定義すると大域環境が壊れない限りカウントが続く.


(define count
  (lambda ()
    (set! n (+ n 1))
    (write n)
    (newline)))
(define n 0)
(count)
(count)
(count)

RESULT

1
2
3

Python, JavaScript, Haskell だとどう書くのだろう.
Python で次のようなコードを書いてみたがうまくいかない.
Python ならジェネレータで対応するような話なのだと勝手に思っているがよくわからない.


def count():
    return n + 1

n = 0
print(count())
print(count())
print(count())

RESULT

1
1
1

次のようなコードが対応するコードだろうか.


def counter():
    num = 0
    while True:
        yield num
        num += 1

count = counter()
for i in count:
    print(i)
    if i >= 3:
        break

print("ここで 4 が出る")
print(count.__next__())

for i in count:
    print(i)
    if i >= 7:
        count.close()

RESULT

0
1
2
3
ここで 4 が出る
4
5
6
7

内部環境によるカウンタ

ここまで n は無防備な状態で大域的にさらしていたので,
どこからどんな変更を受けるかわからず「安全」ではない.
そこで安全なカウンターとして扱うために変数を隠蔽したい.

ところで JavaScript で DOM の扱いが大変とかその辺の話とも絡むと思っている.

それはそれとして count を改善しよう.
変数 n を内部で定義すればいい.


(define count
  ((lambda (n)
     (lambda () (set! n (+ n 1))))
   0))
(write count)
(newline)
(write (count))

RESULT

#<procedure 10b86b1e0 at <current input>:77:5 ()>
#<unspecified>

素数夜曲には「(count) を連打することで, くり返し評価値が返されて, 数値が 1 から順に上がっていく」とあるが,
どうやって n を取り出せばいいのだろう.
いわゆるリターン的なやつはどこに仕込めばいいのか.
これ, lambda が何なのか全くわかっていないからこの程度もわからないのだろう.
とてもつらい.

とりあえず本に沿って let で書き直す.


(define count
  (let ((n 0))
    (lambda () (set! n (+ n 1)))))

RESULT

An error occurred.

何かエラーになった.
どこかタイポしているようだがどこだかわからない.

それはそれとして, 最初のコードに関して
力づくで次のような (私の当初の意図とは違う) 出力を得た.


(define count
  ((lambda (n)
     (lambda () (set! n (+ n 1))
             (write n)
             (newline)))
   0))
(count)
(count)
(count)

RESULT

1
2
3

あくまで count では n の値を返してほしい.

本によれば lambda に先駆ける let, 通称 let over lambda はクロージャを作り,
これがいい感じに振る舞ってくれるとかいう話.
クロージャどころか lambda の理解さえあやふやになってきた.

追記

またもコメントを頂いた.

記事中のこれ

(define count
((lambda (n)
(lambda () (set! n (+ n 1))))
0))

(define count
(let ((n 0))
(lambda () (set! n (+ n 1)))))

は Scheme の手続きとしては誤っていないはずなのでただちにエラーになるものではありませんが、値を返さないのは set! フォームの返却値は未規定になっているからです。

(define count
(let ((n 0))
(lambda () (set! n (+ n 1))
n)))

という風に最後に n を入れれば n の内容を返す手続きになります。

Scheme で言うところの「返却値が未規定」というのは、何を返すかは処理系の裁量で決めていいし、ユーザは何が返ってくるかあてにしてはいけないということです。 代入した値が返ってくるかもしれませんし、変数の名前が返ってくるものも存在しますし、状況に無関係なデタラメな値かもしれません。 Guile の場合は未規定を意味する型のオブジェクトが設けられていてそれが返るようですね。 # と表示されるのはそういう意味です。

return がないが,
やはり Scala や Ruby のように最後に値を入れれば return 的なことはしてくれるのか.


(define count
  (let ((n 0))
    (lambda () (set! n (+ n 1))
            n)))
(write (count))
(newline)
(write (count))

RESULT

1
2

ここで最初 (count)count と素の形で書いていて #<procedure 10a6234e0 at <current input>:149:4 ()> と出力されてはまった.
(count) としないといけないところを count と書いてしまうのは,
根本的なところで Scheme を理解していないという気しかしない.
Emacs LISP を書かねばならないときも同じことをよく思う.
クォートをつけないといけないときもいまだによくわからない.

let 使わない方も書いておこう.


(define count
  ((lambda (n)
     (lambda ()
       (set! n (+ n 1))
       n))
   0))
(write (count))
(newline)
(write (count))

RESULT

1
2

ようやくできた.

generator

この手続きをさらに一般化するために次の generator を作る.


(define generator
  (lambda ()
    (let ((n 0))
      (lambda ()
        (set! n (+ n 1))
        (write n)
        (newline)))))
(define count-1 (generator))
(define count-2 (generator))
(write count-1)
(newline)
(write count-2)

RESULT

#<procedure 10f6900a0 at <current input>:111:6 ()>
#<procedure 10f690080 at <current input>:111:6 ()>

値が返ってこない.
鬱陶しい.


(define generator
  (lambda ()
    ((lambda (n)
       (lambda () (set! n (+ n 1))
               (write n)
               (newline)))
     0)))
(define count-1 (generator))
(define count-2 (generator))
(write (count-1))
(newline)
(write (count-1))
(newline)
(write (count-2))
(newline)
(write (count-1))
(newline)

RESULT

1
#<unspecified>
2
#<unspecified>
1
#<unspecified>
3
#<unspecified>

とりあえず独立したカウンターとして動作することを確認した.
しかし はどこから出てくるのだろう.
(write (count-1)) あたりを削除するとエラーになる.
この程度もわかっていないとかさすがに震える.

カウンターの初期値を設定できる generator を書いて終わり.


(define generator
  (lambda (initial)
    (let ((n initial))
      (lambda ()
        (set! n (+ n 1))
        (write n)
        (newline)))))
(define count-1 (generator -1))
(define count-2 (generator 0))
(write (count-1))
(newline)
(write (count-1))
(newline)
(write (count-1))
(newline)
(write (count-2))
(newline)

RESULT

0
#<unspecified>
1
#<unspecified>
2
#<unspecified>
1
#<unspecified>

これも多分上のコードと同じ変なところがあるはずだ.
Scheme はとても難しい印象を受けた.

追記

count に関してコメントを頂いたことを受け,
書き直してみよう.


(define generator
  (lambda (initial)
    (let ((n initial))
      (lambda ()
        (set! n (+ n 1))
        n))))
(define count-1 (generator -1))
(define count-2 (generator 0))
(write (count-1))
(newline)
(write (count-1))
(newline)
(write (count-1))
(newline)
(write (count-2))
(newline)

RESULT

0
1
2
1

中の lambda の中で n を返すことにしたらエラーにならなくなった.
それ自体はいいのだがまだピンと来ない.
正確に言うならもう一度同じような処理が必要になったとき,
混乱せずに一発できちんとしたコードを書ける自信がない.

じっくりやるしかない.


中高の数学の復習から専門的な数学・物理までいろいろな情報を発信しています.
中高数学に関しては中高数学駆け込み寺,
大学数学に関しては現代数学観光ツアーという無料の通信講座があります.
ご興味のある方はぜひお気軽にご登録ください!

  • このエントリーをはてなブックマークに追加
  • LINEで送る

関連記事

  • コメント (2)

  • トラックバックは利用できません。

  1. 記事中のこれ

    (define count
    ((lambda (n)
    (lambda () (set! n (+ n 1))))
    0))

    (define count
    (let ((n 0))
    (lambda () (set! n (+ n 1)))))

    は Scheme の手続きとしては誤っていないはずなのでただちにエラーになるものではありませんが、値を返さないのは set! フォームの返却値は未規定になっているからです。

    (define count
    (let ((n 0))
    (lambda () (set! n (+ n 1))
    n)))

    という風に最後に n を入れれば n の内容を返す手続きになります。

    Scheme で言うところの「返却値が未規定」というのは、何を返すかは処理系の裁量で決めていいし、ユーザは何が返ってくるかあてにしてはいけないということです。 代入した値が返ってくるかもしれませんし、変数の名前が返ってくるものも存在しますし、状況に無関係なデタラメな値かもしれません。 Guile の場合は未規定を意味する型のオブジェクトが設けられていてそれが返るようですね。 #<unspecified> と表示されるのはそういう意味です。

      • phasetr
      • 2017年 1月18日

      ありがとうございます。
      動くコードを含めて追記しました。

このサイトについて

はじめまして。相転移Pです。数学・物理の情報を中心にアカデミックな話題を発信しています。このサイトを見て興味があればぜひご連絡ください。 mail: phasetr@gmail.com LINE: oxg2753d
  • このエントリーをはてなブックマークに追加
  • LINEで送る

YouTube チャンネル登録

講義など動画を使った形式の方が良いコンテンツは動画にしています。ぜひチャンネル登録を!

メルマガ登録

メルマガ登録ページからご登録ください。 数学・物理の専門的な情報と大学受験向けのメルマガの 2 種類があります。

役に立つ・面白い記事があればクリックを!

記事の編集ページから「おすすめ記事」を複数選択してください。