星田さんの記事に関するコメント。
関数のところでfor-eachとmapって今回の例では別に副作用とか無いのでは?
もう一回整理しますが、入出力自体が副作用です。
つまり、破壊的変更は副作用の一つですが、破壊的変更=副作用ではない、って事です。言い換えると破壊的変更∈副作用、です。
副作用は、もう一回繰り返しますが、主に次のようなモノを含みます。
- 破壊的変更
- 入出力
- ファイル書き込み/ファイル読み込み
- プリンタへの出力指令
他にも、コンピュータでお馴染み(になってる)の「音を出す」なんかも副作用です。まあ、直接的にはプログラミング言語とは関係が薄いジャンルですけどね。
標準Schemeで定義されてる関数では主に次のようなモノが「副作用目的」です。
- bytevector-copy!(Racketでは(require rnrs/bytevectors-6))
- bytevector-u8-set!(Racketでは(require rnrs/bytevectors-6))
- close-input-port
- close-output-port
- define系
- delete-file
- display
- flush-output-port(Racketでは(rnrs/io/ports-6))
- for-each
- newline
- read系(ただし、性質的に、返り値が存在し、返り値が無いと役には立たない)
- set!系
- string-copy!
- string-fill!
- string-set!
- vector-copy!
- vector-fill!
- vector-set!
- with-input-from-file
- with-output-to-file
- write系
!が付いてる破壊的変更系は分かりやすいでしょうが、その他にも副作用目的の関数(と言うか手続き)があります。
まず、read系は機能的には返り値があるんで、それが主作用なのは間違いないんですが、同時に入力ポート(例えばキーボード)から入力信号を受け取る「副作用」があります。と言うかこの副作用がないと成り立たない。
我々の日常感覚から言うと、「キーボードから入力を受け付けたい」からそういう関数を使う為、どうしてもそっちが主作用なのでは?と思いがちなんですが、あくまでコンピュータサイエンス的な「分類」でどっちなのか、と言う話なのです。だから「副作用を目的とした」と言うような言い回しにならざるを得ない。
そしてこの例から分かるのは、副作用目的だから返り値が存在しない、と言うわけでもない。その辺は設計次第だし、仮にread系の関数に返り値が無かった場合、当然「何が入力されたのか」返ってこないわけで、使い物にならなくなります。
それから、意外だろうなのが、「定義」を司るdefineの類ですね。そもそもこれは関数ではなくてマクロの類なんですが、いずれにせよ「副作用目的の」マクロで、「変数に値を束縛する」あるいは「名前に関数定義を束縛する」のはラムダ式を使ったletとは違って副作用範疇です。
この辺がLisp系言語のややこしいトコだ、と捉えるか、「ロクに構文らしい構文もない」Lispの一貫性を示すモノと捉えるか、は人によりますが、そもそもPythonを始めとする各種高級言語で、「関数定義構文で値を返すべきか否か」なんつーのは議論の範疇にならない。それらはプログラミング言語ユーザーが弄れる範疇にないし、言語作成者が提供すべき「構文」の範疇です。
言い換えると、例えばPythonで
def foo():return "hoge"
と関数定義した際に「def自体が返り値を持つべきか?返り値があるとしたら何が適当か?」なんつーのはバカバカしい議論になるでしょう。副作用かどうなのか、なんつーのも考える必要がない。
一方、Lispはマクロがあるんで構文の拡張が出来る。従って、返り値を議論する余地があるわけです。要するに言語設計者とプログラマが同じ舞台に立てる、って事になる。言語組み込みマクロとユーザーが定義したマクロは質として同等、なわけです。
いずれにせよ、関数/変数定義のdefineを始めとする群はScheme系では「返り値を持たない」「副作用目的の」機能です。その辺はScheme系の他の返り値を持たない関数/マクロと全く立ち位置が同じで、取り立てて他と切り分ける必要がないものなんです。
うーん・・この表現から推察するに可変長のリストを引数に取れるというだけのことか・・
その通りです。
ANSI Common LispとSchemeの違いの一つが引数(ラムダリスト、等と呼んだりもするが)への指示記述が違ったりしますね。ANSI Common Lispの方が色々と細やかな記述が可能です。
ANSI Common Lispの場合、オプショナルパラメータやキーワードパラメータを「指定」して記述する事が可能で、可変長引数の場合、&restと言うカタチで指定します。
Scheme系の場合、実装によって拡張されてる場合もあるけど、仕様上はまず、ANSI Common LispやPythonのようなオプショナルパラメータやキーワードパラメータがありません。
ただ、可変長引数の場合
(define (foo . lists) ...
とドットを用いて記述するんで、こっちに慣れてるとANSI Common Lispの&rest記述にはビックリするでしょう。
更に探してこういうのを発見。つまり新しいリストを作らないので、戻り値が必要ない
ここまで読んできたら分かるでしょうが事実上は逆です。
つまり「戻り値を新しく作る必要がないので新しいリストを作らない」が正解です。
今回みたいな表示するだけだった「がゆえに」mapc(for-each)ということだったのか・・
そしてmapcでも返り値が何故にあるのか、と言うのはANSI Common Lispの全関数は必ず返り値がある設計になってるから、です。何度か言いましたが、この辺がSchemeと設計が違うところです。ANSI Common Lispの場合は、副作用目的のブツも含めて全て返り値が存在する。
例えばScheme系の場合、上で見た通り、関数/変数定義のdefineは副作用目的のマクロで返り値が存在しない。
従って、defineで定義を行っても何も返ってこない。
> (define (hoge) "fuga")
>
反面、ANSI Common Lispでは例えば、関数定義用マクロのdefunには「返り値がある」んです。
CL-USER> (defun hoge () "fuga")
HOGE
CL-USER>
つまり、関数定義を行った時点で、その返り値は関数名であるシンボルになってます。
従って、こんな事が出来る。
CL-USER> (defparameter foo (defun bar() "baz"))
FOO
CL-USER> foo
BAR
CL-USER>
大域変数fooに関数定義defunが返す返り値が束縛出来る。こんなアホな事はSchemeには出来ません(また出来る必要もない・笑)。
従って、これから予想出来るのは、ANSI Common LispのmapcとSchemeのfor-eachはどちらも副作用目的のマッピング関数なんだけど、前者には返り値はある。後者には返り値がない、と言う事です。
CL-USER> (mapc (lambda (x)
(format t "This list has ~A~%" x))
'(1 2 3 4))
This list has 1
This list has 2
This list has 3
This list has 4
(1 2 3 4) ;; 返り値がある
CL-USER>
;; (require srfi/48)> (for-each (lambda (x)
(format #t "This list has ~a~%" x))
'(1 2 3 4))
This list has 1
This list has 2
This list has 3
This list has 4
> ;; 返り値はない
「新たなconsを行わない事」以外は全部副作用ということなの・・・か・・・?
と言うわけで、この辺は勘違いだ、って事はもう分かると思います。
新しくconsingするかどうか、ってのは関係なくって、あくまで「高階関数であるマッピング関数でどういう関数を適用したいか」と言うのがmapcやfor-eachを使うかどうか、の理由です。
ANSI Common Lispの場合特に、新しくconsingするかどうかではなく、
CL-USER> (mapcar (lambda (x)
(format t "This list has ~A~%" x))
'(1 2 3 4))
This list has 1
This list has 2
This list has 3
This list has 4
(NIL NIL NIL NIL)
CL-USER>
返り値の(NIL NIL NIL NIL)ってリストを再利用する価値があるかどうか、ってのが大きい。
そしてなんでNILだらけなんだ、っつーのも、
CL-USER> (format t "hello, world!~%")
hello, world!
NIL
CL-USER>
と、関数formatの返り値がNILだからに他なりません。そしてANSI Common Lispの出力関数の返り値がNILになる、って決まりごとも特にはないです(例えばprincは出力に使ったデータを返す)。
つまり、何らかの出力関数をマッピングした際に、関数に返り値があって、それを再利用するかどうか、ってのが大きいのです。mapcの場合は
「受け取ったリストをそのまま返せば、何かでまた使い回し出来んじゃねーの?」
と言う理由の方が恐らく大きく、スピードがどうの、って実装上の理由はあんま関係ないと思います。memberなんかもそうだけど、ANSI Common Lispの場合、
- 副作用目的でも返り値を返さないとならないんで、どうせ返り値を返すのなら使い回し可能な値を返すようにしたい
と言うカンジで設計されてるモノがあるのです。
余談。
例えばコンピュータサイエンス的に言うとC言語のprintfも「副作用目的の関数」です。そして「関数な以上返り値があり」、結果を代入出来ます。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
printf("%d\n", printf("hello, world!\n"));
return EXIT_SUCCESS;
}
printfを二重にしてる、ってビックリするかもしれないけど、文法的には実は正しい。
コンパイルして実行すると、
➜ ~ ./a.out
hello, world!
14
➜ ~
となって、副作用でhello, world!が表示されますが、後者の14ってのは何でしょう?
実は"hello, world!\n"の文字数が内側のprintfの返り値で、それが外側のprintfの書式指示子にハマって表示されてる、ってのが上の不可思議なコードの内訳です。
いずれにせよ、「関数が何を返り値にするのか」と言うのはその関数の設計者に負う場合がある、と言う例です。