見出し画像

Retro-gaming and so on

逐次実行の話

10年以上前?プログラミングを初めた頃。
全く関係ないんだけど、とある数学系サイトに入り浸ってたりしてた(教えて!gooではない)。
そこで、理由は割愛するんだけど(と言うのもしょーもない理由だ、ってのが後に発覚したからだ・※1)、プログラミングの質問が良く投稿されてたんだよな。
僕はLisp、特にScheme専門、で当時は他のプログラミング言語の知識はそんな無かった。いや、今はある、たぁ言わないけどな。
いずれにせよ、そこで投稿されたプログラミングの質問は殆どがPascalに付いての質問だったんだ。10年以上前でもPascalは主流言語からすでに外れてた、と言うのもMicrosoftのVisual C++がWindowsでは「勝った」からだ。
要は1980年代と違って、Pascalを「知る」人はあんまいない。つまりレスが付かない。その割にはやたらPascalでの質問が投稿される。
しょーがないんで「読んでみる」んだよ。全くPascal知らないのにさ。「Lispで培った知識」で何とか解釈しようと試みた。Pascalが分からんなりに「書こうとしてる」ロジック自体は把握出来るのでは、と。
これが意外と上手く行くんだよ。「読んでも」書けるようにはならんが、「読むこと」自体は可能だった。これには我ながらビックリしたもんだ。
理由は2つある。一つは「プログラミング言語は自然言語ではない」。人工言語だ。Artificial Languageだ。
英語話者でさえ構造が似ているドイツ語で書かれた文章を、ドイツ語を知らなければ「解読」するのは無理だろう。文字通り「何が書かれてるのか」分かるわけがない。なぜなら「そこに書かれてる事」には無限の可能性がある、からだ。
ところが、プログラミング言語ってのは書いてる事は結局のトコ次の3つしかないんだ。

  1. 逐次実行
  2. 条件分岐
  3. 反復
これだけ、だ。これがエドガー・ダイタクストラが言った「構造型プログラミング」の3要素だ。一般的にはどの言語もこの3つの機能「だけ」は揃えてるんで、それだけ知ってれば何をやろうとしてるのかは殆ど1対1対応で解読する事は出来る。ロゼッタストーンのヒエログリフを解読した古代エジプト学者、ジャン=フランソワ・シャンポリオンみてぇに苦労する必要はない。
もう1つの理由は、単に僕がLispをやってたから、だろう。それはこういう事だ。
仮に「機能がない」プログラミング言語の知識で「機能が多い」プログラミング言語で書かれたソースコードを読むのはそりゃ難しいだろう。「機能がない」プログラミング言語から見れば「機能が多い」プログラミング言語にどんな機能があるのか想像する事すら難しいから、だ。
しかし、Pascalは、ハッキリ言っちゃえばC言語並に「機能がない」言語だ。っつー事はLispから見ると「想像も出来ない」ような機能を持ってる事はあり得ない。逆にPascalの知識しかないのにLispコードを読むのは難しいだろう。Lispには「Pascalじゃ考えられないような」機能が満載だから、だ。
ここでも「ほげ言語のパラドックス」が効いてくる(※2)。
そんなワケで、「全く知らないプログラミング言語で書かれたソースコード」の「ロジックを理解」する事は出来る、って事を発見した。書けなくても読めはするんだ、と。
そんなワケで、質問してる質問者よりも「僕の方が」プログラムを「読む」事に関しては上達していったわけだ。

ところが、稚拙な質問に書かれてる「分かりません(だから教えてください)」に対して説明していったら、ちと意外な事が分かってきた。「分かりません」な人は「分かりません」なわけだが、それ以前にプログラムのソースコードが「読めない」。
もちろん、ソースコードが「読めない」から「分からない」わけなんだけれども。特徴的な事が分かってきたんだわ。
みんな、プログラミングと言えば「条件分岐」とか「反復」とか、そっちを中心に考えたり教えたりしてるわけなんだけれども。
実はこのテの「全くプログラミングが出来ない」奴らってのはどうもそれ以前で既に躓いてる。
そう、実は逐次実行の時点で理解出来てないんだ。
逐次実行ってのは耳慣れない言葉だろうが、要はファイルに上から書いた順番に動作すると言う事が分かってない。
そしてあまりに基礎的な事なんで教科書なんかでは、ここは確かにサラッと流しがちだ。しかし「プログラミングが出来ない層」ってのはこの「ファイルに上から書かれた順番に動作する」って事が分かっていない。

きしださん、って言うその道では有名なJavaハッカーがいるんだけど、こんな事を書いている

けど、実際には上から順に動くというのがよくわからないようです。
「あ、プログラムって上から順番に実行されるんですね、わかってなかった」
と言われたことがあります。そういうふうに言ってくれる人がいるということは、言わないけどわかってなかったという人が何倍もいるはずです。

そう、実はその通りなんだ。
僕も「プログラミングが分からない」って言う人を、Web上でそれこそかなりの数を見てきた。
彼らは条件分岐や反復で引っかかってるわけじゃない。それ以前のトコで引っかかってるんだ。
人類は、素で逐次処理を当たり前、って思える人とそうじゃない人に二分されている

そして、実の事を言うと「逐次処理」を理解できない人はかつてより増えている。
理由を説明するが、まず前提を確認しよう。
原理的には「コンピュータは一回に一つの事しか出来ない」。
「え?」とか思う人がこの時点でいるんだ。しかしそれは当然の反応だ。

「だって俺、ワードとエクセルを同時に使ってるぜ?エロ動画見ながら、な。」

そう、現実問題、ワードとエクセルを同時に立ち上げながら、エロゲをしたりエロ動画を観たり出来る世界に僕らは住んでいる。



この世のコンピュータは既にマルチタスキングなんだ。
しかしマルチタスキングにはトリックがある。依然と原則は、コンピュータは一度に一つの事しか出来ないシングルタスク、ってのが基本的性質なんだ。

いや、昔のPCはマジモンでシングルタスクだった。一度に一つのソフトを走らせる事しか出来なかった。


昔のPCはエロゲを起動したら終了(quit)するまでそれしか出来なかった。

だから、1PCユーザーが「プログラミングを始めたい!」と思った時に、比較的、「逐次処理」ってのは理解しやすかったんだよ。「一度に一つの事しか出来ない」以上、命令も「順次に実行するしかあり得ない」って思えたからだ。
ところが、現代はマルチタスキングが当たり前だ。「色んなソフトを」同時に起動出来る。
つまり、プログラミングの「逐次実行」って概念がピンと来ないんだ。現実で使ってるPCがマルチタスキングである以上、「コンピュータは一度に一つの事しか出来ない」前提で、「ファイルに動作を上から順番に書いていって」も、その書いたすべてが「同時に実行される」ように勘違いしかねないんだわ。
多分、現代のマルチタスキング環境を鑑みると、「逐次実行」ってのは「改めてキチンと解説しなきゃアカン」概念になっている。単純に現実の「PCの動作」と既にそぐわないから、だ。

余談だが、実は今でもコンピュータはシングルタスク前提だ。
実際は「シングルタスクしか出来ない」のをマルチタスキング「してるように見せかけてる」ってのが本当のトコだ。
これが出来るのは、「コンピュータは一つの仕事を行う」のが人間では考えられないくらい「高速度」で行なわれているから、だ。
ちと歴史的背景を説明してみよう。
大型コンピュータの時代でもコンピュータは当然「シングルタスク」、つまり「一回に一つの事しか出来な」かった。
この時代のコンピュータの「使用形態」ってのは最初は次のようなカンジだったんだな。まず「紙でプログラムを書く」(笑)。いや、マジで。で、出来ればそれを「パンチカードで打ち直す」(笑)。そこ、笑ってんじゃねぇよ(笑)。いや、俺も笑いながら書いてるけど(笑)。
そしてそれをメインフレームが鎮座してる「コンピュータ室」に持っていくんだ。予約して。そこでその部屋の管理者が、そのパンチカードをコンピュータに食わせる。そこでコンピュータが計算してくれて、結果を返してくれるわけだ。そして管理者は言う。「次の人、どうぞ」と(笑)。
殆ど病院の待合室の様相だ。しかもバグが出てうまい具合にプログラムが動かなかったら「では3日後に正しく書いたパンチカードを持ってきてください」と管理人に言われる始末だ。
実はこの状況が暫く続くんだわ。パンチカードの代わりに自分でプログラムを直接タイプ可能な端末が出てきたが、基本変わらん。コンピュータを端末込みで使いたくてもコンピュータはシングルタスクなんで、一度に一人しか対応出来ない。つまり、やっぱコンピュータを使用したい場合は時間を予約して決められた時間に単位時間毎に借りるしかなかったんだわ。
コンピュータを使いたい、って人間は増えたが、稼働時間を人々に割り振るのが大変だ、と(※3)。
ここまで書いてきたら分かると思う。必然的に出て来たアイディアは、マルチタスクより先にマルチユーザーだったんだ。一台のコンピュータの「貸出時間」を何とか複数人で共有出来ないか、と。古い言葉で、このアイディアをそのまんまタイムシェアリングと呼ぶ。
このタイムシェアリング、と言うアイディアは複数の人によりほぼ同時期に提唱されたんだけど、一番有名な人がご存知、Lispの発明者、ジョン・マッカーシー博士だ。



この人、実は今で言うコンピュータオタクで、コンピュータ大好きっ子だったらしい(笑)。元々、ダートマス大学ってトコで教えてたんだけど、「MITの方が最新で性能がいいコンピュータが使える!」ってぇんで移籍したらしい(笑)。どんだけ好きなんだよ、ってな話だ(笑)。
この「タイムシェアリング」ってアイディアも、「時間内に"自分だけが使える"状態をなるたけ作り出したい」から来たらしい(笑)。ある意味「自分の欲望に忠実」だと言える(笑)。
さて、この「タイムシェアリング」と言うシステム。原理的には簡単なんだ。CPUは一つしかない。んでメモリがある、と。
元々、メモリは単純に言うと、中に「命令とデータ」がガーッと並んでて、CPUはそのメモリの「先頭」から順次実行していくわけだ。
このメモリを「許容出来るユーザー数」に分割する、と。そしてそれぞれに「先頭」があるわけだな。そしてその中に「計算の途中経過」の状態もある、と。
そしてOSが「今実行中の全部分」を把握してるんだけど、高速でその「今実行中の部分」を切り替えていくわけだ。例えば4人が使用してる、と言う前提だとメモリを4分割して、ユーザー1、ユーザー2、ユーザー3、ユーザー4、と高速で切り替えていく。そうするとユーザー側の視点から見ればあたかも「4つのコンピュータが別々にある」ように見えるわけ。
原理的な仕組み自体は簡単だろ(笑)?
ちなみに、Linuxなんかも実はマルチユーザーのOSで、例えば僕が許可すれば、僕が使ってるPCを他の人が「ネット越しに」動かす事が可能なんだ。僕がログインしてるだけじゃなくって「別の人がアカウントを僕のPC上に作って」僕が僕の個人PCを使ってる間でも僕の個人PCをシェアして「同時に(別々の目的の為に)動かす」事が出来る。
一方、Windowsの場合、家族の一人一人がWindows上に「アカウント」を作れるけど、「複数人が同時に使える」ようには設計されてない。それも別にMicrosoftの技術力が無いから、じゃなくって最初っから「パーソナルコンピュータ用のOS」として設計されてるから、だ。一台のマシンを複数のユーザーが「同時に使う」のは(商売的に見て・笑)、端から考えていない。
さて、「マルチユーザー」及び「タイムシェアリング」の技術を応用すればそのまま「マルチタスク」になる、ってのは何となく分かるだろう。根本的な技術的発想に於いて、この2つは実は全く同じだ。「メモリをユーザー毎に振り分ける」のではなく、「プログラム毎に振り分ける」ようにすればマルチタスクの完成だ。アドレスaからはエクセル、アドレスbからはワード、アドレスcからはエロ動画、アドレスdからはエロゲ、と振り分け、それぞれの「実行過程」を高速で切り替えるとユーザーの目からは「複数のソフトを同時に使ってるように」見える。そしてそれぞれのメモリの管理はOSが行っていて、それが「マルチタスクのOS」がやってる事、なんだ(※6)。

とまぁ、この「マルチタスク」の仕組みがWindows NT以降「当たり前」になったせいで、「コンピュータは原理的には逐次実行しか行えない」と言う事を「説明されなければ分からない」ようになってしまった、んだ。プログラムは逐次実行で記述されなければならない、と言う「縛り」は実はコンピュータの原理的な「仕組み」から来てるんだが、現実問題としてその「関係性」が切れてしまった(ように見える)為、直感的にピンと来ない状況になっている。

本論に戻ろう。
「プログラミング入門書」の良心的な本であれば、まずは「逐次実行」をキチンと説明しているハズだ。
別な言い方をすると、実は「オッサンになればなるほど」逐次実行、ってのが「常識化」し過ぎて、この前提がスッポ抜ける。何故ならオッサンにとっては「かつてのコンピュータは一度に一つしか出来ない」事が常識だった為、「現代の若者を巡る環境」に思いが至らない(笑)。PC-6001時点での「常識」と今のWindows機ユーザーの「常識」が違う、って事がピンとこないんだ(笑)。
かつてのPython.jpには非常に良い、短いプログラミングチュートリアルがあって、こういう事が書いてあった。



この「料理のレシピ」は割にプログラミングのメタファで良く使われる。「手順が上から順に書いてあって」、上から一つづつ自分でやっていけば「料理が出来上がる」ってのはなるほど、確かに「プログラミング」に似てる。



この文書は実はPython 2.x 時代のモノで、現行のPython 3と違ってprintが文だ。
現行のPythonだと次のようにprint部分を修正しとかないと動かない。

# 長方形の面積計算

# 材料

width = 20
height = 30

# 調理法:

area = width * height
print(area)

うん、確かに「料理のレシピ」の記述手法に似てる。クックパッドの記事も多かれ少なかれこんなカンジではある。
確かにフツーのプログラミング言語では、このテの「逐次実行」が基本で、サンプルプログラムもこのテのモノだ。
しかし、ちょっと待った。このプログラムを素材として考えてみるが、実はこのプログラム、その殆どが、コンピュータを使う目的である計算じゃないんだ。

# 長方形の面積計算

# 材料

width = 20 # 代入は計算じゃない
height = 30 # 代入は計算じゃない

# 調理法:

area = width * height # 計算はしてるが代入は計算じゃない
print(area)     # 出力は計算じゃない

実は書かれた命令は4行だが、3/4は「計算とは全く関係がない」行為なんだ。
ここまで「プログラミングの基礎としては逐次実行は大事だ」とは書いてきた。
一方、実の事を言うと、一般に、「逐次実行」で実行される部分のその殆どは「計算とは関係ない」んだよ(笑)。
つまり、タイトルから始まってここまで「逐次実行はプログラミングの基礎」ってスタンスで来たが、ここからは「本当にそうか?」って話にしていく(笑)。

上のコードをRacketのようなLispで書いてみる。
こんなカンジになるだろう。

;; 長方形の面積計算

;; 材料
(let ((width 20) (height 30))
 ;; 調理法
 (let ((area (* width height)))
  (printf "~a~%" area)))

確かにロジックは似てはいるんだ。
ただし、実はいくつか違いがある。

  1. Pythonでは「代入」は変数と値の「束縛」を生むが、Racket(Lisp)のletはそれだけではなく、「本体」部の「実行」を司る。
  2. Pythonではwidthheightareaの3つは構造的には「すべて独立した変数」だが、Racket(Lisp)ではwidthheightの2つが1グループであって、変数areawidthheightの2つから作り出されるモノだ、と言う関係性が明確になっている。
  3. Racket(Lisp)のコードは事実上、「逐次実行」を表していない。実行される部分はlet本体に置かれた(printf "~a~%" area)だけだ。
まずは3番に着目しよう。そう、実はRacketのコードは「逐次実行」じゃないんだ。出力は依然副作用で「計算ではない」んだけど、実行されてるのはその「出力だけ」だ。
そして1番。実はletlet本体部に置かれた式を「実行する」能力があって、スクリプトは実行されます、とか言うような事とは違うレベルで動いている。
と言うのも、実は原理的にはletは関数なんだ。
実はこれはラムダ式を使った次の表現へと変換されている。

;; 長方形の面積計算

((lambda (width height)
 ;; 調理法
 ((lambda (area)
  (printf "~a~%" area)) (* width height))) #| 材料 |#20 30)

従って、(let ((width 20) (height 30))...と言う表現部は関数の仮引数に実値を与えてるだけで代入してるわけじゃないんだよ。
「いや、それがどーした」って思うかもしんない。「それはLispだから、だろ?」と。
違うんだ。実は逐次実行ってのは時としては必要だけど本質的には必要なモノではない、と言う事を言ってるんだ。
Lispがそれを証明してる。Pythonの件の例をLispでは逐次実行で実は書いてない、と言うのは、同じ事を実行するのに逐次実行は要らない、って事を言ってるんだよ。
事実、ラムダ式を使えるPythonではLispでのletを使った「内情」を次のように書けてしまうんだ。

# 長方形の面積計算

(lambda width, height: (lambda area: print(area))(width * height))(20, 30)

「うげぇ」って思うだろ(笑)?
いやだから、こんな風に「書け」って言ってるわけじゃない。単に理論的な話だ。
そして「理論的」にはこのラムダ式と括弧塗れの「組み合わせ」が、上で紹介した「逐次実行」の「優しい例」と等価だ、って事を言ってるんだ。
嘘のようなホントの話だ。
そう、そもそも代入自体が文として別に書かなアカン、って事(必然性)は無い、って言ってるんだよな。
そしてPythonのラムダ式はLispのラムダ式と違って「複数の式が取れない」。「複数の式が取れない」クセに剥き出しの複文だらけだったオリジナルと同じ効果のコードが書けている。これが明確に指す意味は何なんだ、と。
さて、本当に逐次実行は必要なのか?

に「フツー、どんな言語でも逐次実行と言う機能はある」と書いた。
しかし、実の事を言えば、Lispはその誕生時、その逐次実行が無かった
つまり、「逐次実行が無くてもプログラムは書ける」と言うのが誕生時のLispの主張だったんだ。
嘘!って思うかもしれないけど、上のプログラムと同等のプログラムは関数を利用して次のようにしても書ける。

;; 長方形の面積計算

;; 材料
(define (width x) x)

(define (height x) x)

;; 調理法
(define (area x y) (* x y))

(printf "~a~%" (area (width 20) (height 30)))

若干冗長に見えるかもしんない。が、Lispでは逐次実行を行うより関数の引数と返り値を利用し、つまり関数を連鎖させて書くプログラミング言語として登場した
これが関数型プログラミングの原初的なアイディアだ。上のコーディング例を見れば分かるけど、変数への代入行為は必要ない。従って、逐次実行も必要がない、んだ。
Pythonだと次のようになるだろう。

# 長方形の面積計算

# 材料
def width(x):
 return x

def height(x):
 return x

# 調理法
def area(x, y):
 return x * y

print(area(width(20), height(30)))

ここでも「変数の代入行為が必要ない」ので、逐次実行が付け入る隙がない。
そう、逐次実行ってのは実は副作用が要してる、と言う前提で必要になる機能なんだ。
逆に言うと、副作用を使わない場合、必ずしも逐次実行はプログラミングの必須要素にはならない、と言う驚くべき事実がある
これはLisp登場時では衝撃的なメッセージだったんだが、多くの人の前にLispが姿を表した時、さすがに「逐次実行無しだとプログラミングしづらい」と言う人がテスト段階で続出したため(笑)、逐次実行を含んだプログラミング言語となっていた。

しかし、気づく人は気づくだろ。上のLispやPythonのコードを見た時点で「冗長だ」と感じた人は、

だったら最初からまとめて関数にして書けばいいんじゃね?

と思うだろう。その通りなんだよ。
どう見てもこう書いた方がスッキリする。

;; Racket の場合
;; 長方形の面積計算

(define (area width height)
 (* width height))

(printf "~a~%" (area 20 30))

# Pythonの場合
# 長方形の面積計算

def area(width, height):
 return width * height

print(area(20, 30))

そう、だからこそ、Lispの教科書なんかでは逐次実行を説明しない(※7)。と言うか、フツーのプログラミング言語と全く違う「順序立て」で説明していく。
Lisp系の教科書の場合、最初にインタプリタで「式一個を実行する」例を出す。足し算とか引き算だよな。その辺はPythonなんかと同じだ。
次の段階でいきなりリストを説明する。まあ、LispはList Processorの略だ、とか言われてるくらいなんで当然と言えば当然なんだが、フツーの言語の「配列」の登場時に比べると遥かに早いんだ。
例えばC言語のK&Rなんかでは第一章で配列は登場するが、「反復」や「条件分岐」等の制御構文の後に登場する。

 
これは他の「C言語入門書」でも似たようなモンだ。配列はかなり後に出てくる。
と言うのもC言語だと「配列はそのままじゃ使えない」、言い換えると制御構造がないと使いようがないんで(特に「反復」を学ばないと印字さえ出来ない)、こういう配置になってんだ。
だからプログラミング初心者が、C言語の制御構造を覚えてヘトヘトになった後に「配列登場」で、「まだ覚える事があるんか」と、丁度「抵抗感を感じる」辺りで出てくるわけ。だから反射神経的に「配列への苦手意識」が刷り込まれる。
そしてK&Rの章構成に影響を受けてる、他言語(例えばPython)の入門書でも似たり寄ったりの配置になる。K&Rが正しいと言う根拠のない章配置になってんだよな(※8)。
一方、Lispの場合は、見方に拠っては敷居が高く、いきなり配列に相当するコンテナ型のリストを導入する。conscarcdrlistだ。複合データ型をいきなり学ぶ、んだ。
そしてその後、すぐ関数定義を紹介する。制御構造より前に、だ。条件分岐や反復技法は関数定義のに導入する。この辺で再帰が出てくるわけだが、再帰で反復する以上、関数定義の知識は必須だから、だ(※9)。
紫藤のページを見てみよう。これが典型的なLisp教授の「章立て」の順番だ。すぐリストだ。すぐ関数定義だ。
いや、何もK&Rに対抗して「Lispの章立ての方が正しい」って言ってるわけじゃないんだ。そうじゃなくって「言語の特性に合わせた章立てじゃないと意味がない」って言ってるんだよ。C言語の場合、配列が制御構造の後に来ざるを得ないのは、制御構造を先に学んでないと配列の使いようがないから、だ。印字も出来やしない。
じゃあK&R的な章立てでPython本を書くのに何の意味があるんだ、って事を言ってるんだ。Pythonならリストをそのままprintに渡せばそのまま印字可能だ。制御構造の後にリストを紹介する必然性なんかあるんか、って言う話だ。
プログラミング言語入門書の章構成で、その本の著者がどれだけその言語の事を熟知してるか大体分かるわけ。K&Rの章構成をパクっただけのPython入門本とか、それだけでクソだと言う事が簡単に分かる。

教えて!gooから引っ張ってきたんだが、次のような問題を考える。



例えばこれをRacketで直球勝負で書けば次のようになるだろう。

(require (only-in srfi/13 string-tokenize))

(define (foo)
 (let ((sc (map string->number (string-tokenize (read-line)))))
  (let loop ((votes (make-list (car sc) 0)) (M (cadr sc)))
   (if (zero? M)
    (index-of votes (apply max votes) =)
    (loop (let-values (((head tail) (split-at votes (read))))
        (append head (cons (add1 (car tail)) (cdr tail))))
       (sub1 M))))))

オリジナルはJavaの問題だが、Javaより遥かに短く書ける。
しかし、ちょっと注意して欲しい。このコードは「関数型プログラミング」の形式なんだけど、実は「命令的プログラミング」の形式が入ってる。letだ。
letによる変数の束縛は便利で欠かせない。
しかし、もう一回ポール・グレアムによる助言を思い出そう。

Lispオペレータのうち,副作用のために呼ばれるよう意図されているものはほんの僅かだ. 一般的に言って,組み込みオペレータは返り値のために呼ばれるよう意図されている. sort,removeやsubstitute等の名前に惑わされてはいけない. 副作用が必要なら,返り値をsetqで代入すること.
まさにこのルールが,副作用を不可避なものにしている. 関数的プログラミングを理想とするというのは, プログラムが決して副作用を使ってはいけないということではない. ただ必要以上に使うべきでないということだ.
この習慣を育てるには時間がかかるかもしれない. 一つの方法は,以下のオペレータは税金がかかっているつもりで扱うことだ:
set setq setf psetf psetq incf decf push pop pushnew
rplaca rplacd rotatef shiftf remf remprop remhash
あとlet*もそうだ. この中に命令的プログラムが潜んでいることがしばしばある. これらのオペレータに税金がかかっているつもりになるのは, よいLispのプログラミング・スタイルへ向かう手助けとして勧めただけで, それがよいスタイルの基準なのではない. しかし,それだけでもずいぶん進歩できるだろう. 

この中にletは含まれていない。letには副作用がない。ただし、その発想はかなり、FortranやBASICみたいな命令型プログラミングの気があるんだ。変数に値を代入する、ってのはそういう事だ。
しかも、Lispにletが登場したのはその登場から10年以上経ってから、なんだ。「便利だから導入された」んだけど、実は形式は文句なしの関数型プログラミングなんだけど、発想自体は命令型プログラミングのもの、なんだ。
じゃあ、最初のLispだったら上の問題はどうプログラミングしてたんだろう。僕らの目の前にLispが登場した時には既に逐次実行機能を備えていたんで、想像するしかないんだけど、多分Lispの総初期ではこんなスタイルでプログラムを組んでたんじゃないか。

(require (only-in srfi/13 string-tokenize))

(define (foo sc)
 (bar (make-list (car sc) 0) (cadr sc)))

(define (bar votes M)
 (if (zero? M)
  (index-of votes (apply max votes) =)
  (bar (baz votes '() (read)) (sub1 M))))

(define (baz votes acc M)
 (if (zero? M)
  (append (reverse (cons (add1 (car votes)) acc)) (cdr votes))
  (baz (cdr votes) (cons (car votes) acc) (sub1 M))))

(foo (map string->number (string-tokenize (read-line))))

全部の関数の本体は返り値の一つのみ、だ。そして関数が連鎖されている。
いや、「こういうスタイルで書くべきだ」って言ってるわけじゃない。明らかにletは便利だ。色んな事を短く出来る。
ただし、「変数への代入」とそれによる「逐次実行」と言う「発想」は必ずしもプログラミングには必要がない、と。その証明ではあるんだ。
要は「逐次処理にまつわるアレコレ」が「絶対必要だ」と言う神話がここにはない。
もっとも見て分かるけど、別なトコで冗長になるのは事実で、letも逐次処理も無かった時期のLispは、確かにフツーの発想じゃプログラミングしづらかっただろう(笑)。「縛りプレイ」に思える(笑)。
しかし、これらがいつか書いた通り、Lisperが不必要な代入を直感的に避けてる原因でもあるんだ。
「変数への代入は、計算結果に"名前"を付ける事で明解になる。」
ただし、それは「命令型プログラミング」なので、関数型プログラミング的にはそういう「発想」をなるたけ減らすようにする。
「逐次処理による可読性」と「関数型言語発想」の間にもトレードオフの関係があって、コーディングスタイルに於いて、「丁度良いのはどの辺か」とLisperはいつもバランスを取ろうとしてるわけだ。

さて、例えばC言語のような言語。何度も書いてるが、原理的に配列も返せないようなプログラミング言語だと、配列を大域変数なんかにして、そこに含まれる値を破壊的変更しないとプログラムにならない。そして破壊的変更は代表的副作用だ。
そしてこのテの「返り値が無い操作」は当然逐次実行の一要素になり得る。っつーかそうやってプログラムせざるを得ないんだ。

関数型言語に始まって、最近だと新しく登場した(とは言っても登場から10年は経ってるが)システムプログラミング言語Rustなんかは変数に再代入をさせないような設計になってきてる。つまり、プログラム中に含まれる副作用が圧倒的に減るような設計になってんだよな。
ポール・グレアムのように、「すべての言語はLispに近づいている」と言う気はないけど、あらゆる新しいプログラミング言語は関数型言語に近づいている。少なくともその機能を貪欲に取り込もうとしてるんだ。

まとめよう。
最近の若者(具体的には1990年代に生まれた世代以降)には「逐次処理」は自明のモノじゃなくなっている。そしてプログラミング言語も「入力」と「出力」以外は副作用を必要としない言語へと進化を始めている。
逐次処理と副作用は密接な関係がある。そしてそれらを使う時は、構造化プログラミング以前の「命令型プログラミング」の発想をしている。
そんな中で「逐次処理」のコンセプトはキチンと説明しなければならない段階に来てるんだ。そして、いつ「逐次処理」がコードの明解性を産むか、そうじゃない時はどういう場合なのか、をハッキリと説明せねばならない「時代」になってるんだ。

※1: ちなみにこの辺で「宿題丸投げ組」と言う奴らにはじめて遭遇する事になった。彼らは「書かれたプログラムを理解したい」わけじゃない。誰かに代わりに答案を作って欲しいだけ、だと。
そして質問が極めて稚拙だ。
実はビックリしたんだけど、この「宿題丸投げ組」は全員「現役教師」だったんだ。小中学校の教員だったんだよ。
何か大学通信講座があって、そこでキャリアアップの為に今持ってる教員免許と別の学科の教員免許が欲しい、と。要は数学教師の免状だな。
そこの通信講座のカリキュラムに「プログラミング」が含まれてて、その指定言語がPascalだったわけだ。テキストが1990年代から改訂されてなかったんで、それが理由で「Pascal」だったんだ。
いずれにせよ、学生に「勉強しろ」とか言う輩の実態は「自らは勉強しない」奴らなんだ。
こんなモンだ。既に教職は「聖職」でもなんでもないし、大学でも教育学部の奴らが「一番勉強しないで遊び回ってる」のは、実はみんな見て知ってるだろう。

※2: 同程度の抽象度の低い言語、つまり、C言語をやってる人がPascalコードを読んだり、あるいはその逆をする、って事自体は、だから「そんなに難しくない」。
一方、実は「C言語しかやった事がない」人が、熟練のPythonハッカーが書いたソースコードを読むのは「凄い難しい」んだ。リスト内包表記が無い言語ユーザーがリスト内包表記を見て「一発でその内情を理解する」って事はほぼ「あり得ない」し、PythonにはC言語では考えられないような「機能」が山ほどある(これがC言語脳が、C言語に「無い」機能を見て「難読だ」と騒ぎ出す原因だ)。
1行で「何が行われてるか」を把握するのが現実問題「出来ない」し、従って、勉強しなければ、決してPythonハッカーのようには「書けない」。
そしてC言語脳は「勉強しない」。彼らは「C言語はすべての基礎」と言う世迷い事を諳んじる、って事しか「出来ない」んだ。それは「都市伝説」の類なんだけどな。
あるいは「迷信」っつって良い。
逆に言うと、「高抽象度のプログラミング言語」を知ってれば、それより抽象度が低いプログラミング言語での「記述」はある程度予想が付くわけだ。何故なら高抽象度であればあるほど「機能が豊富」だからだ。故に知らない言語でもその言語より低抽象度なら「何をやってるのか」アタリが付けやすくなる、ってぇわけだ。

※3: ちなみに、日中はどうしても混むんで、結果、MITみたいな「ハッカーの聖地」ではこの時代、ハッカーの卵は圧倒的に「人が空いてる」夜間(真夜中とか・笑)にプログラミングを行う事となった。ハッカーが夜行性、ってのはこの時代の名残に違いない(笑)。

※4: 米国のアイビーリーグの一つ。名門校。BASICを開発した大学で、コンピュータ教育の先進校の一つだった。そもそもここに勤めたのも「コンピュータ大好きっ子」だったからじゃないか(笑)。

※5: Windowsでそれをやりたい場合は、通常のWindowsではなく、Windows Serverと言うOSを購入してインストールしないとならない。

※6: 民生機では現代のWindows 95以降のWindows以前だと、Macintoshでは「疑似マルチタスク」と言うのが最初に来た。これは複数ソフトを「同時に使ってる」ように「見せかける」技術で、実はシングルタスクOS以上のモノではない。
この時期に「マルチタスク」と「疑似マルチタスク」の差は、前者は「ワープロを使ってる間にフロッピーディスクのフォーマット」が出来たが後者は「フロッピーディスクのフォーマット中は他に何も出来なかった」んだ。
この時期に、民生機で「マルチタスク」が出来たマシンはCommodore Amigaしかなかった。

※7: 厳密に言うと、「逐次実行を説明しない」のではなく、「逐次実行を実現する機能を(そのうち)紹介する」って言った方が正しい。
原理的に、Lispは「自然と逐次実行を含む」プログラミング言語じゃなく「明示的に逐次実行させる」言語で、それがprogn(ANSI Common Lisp)やbegin(Scheme)なわけだ。
だから理論的には、Lispの関数は「単一の式」しか含められず、例えばRacketで「逐次実行させる関数を定義したい場合」、本来なら

(define (foo arg)
 (begin あれをしろ
    これをしろ
    ...))

とか

(lambda (arg)
 (begin あれをしろ
    これをしろ
    ...))

書くべきなんだが、さすがにこれだとメンド臭いんで、begin無しでも逐次実行が出来るようになっている。
これを暗黙のbeginとか暗黙のprognと呼ぶ。
言い換えるとPythonのラムダ式が単一の式しか取れない、と言うのは不便だけど理論的には正しい。単純にPythonの欠点は逐次実行を「指定する」機能(Lispのbeginprogn)がない、って事なんだ。

※8: ちなみに、Lispハッカー、ポール・グレアムはK&Rに対して

言語はそれについて書かれた良い本を必要とする。その本は薄くてうまく書かれていて、 また良い例がいっぱい詰まっていなければならない。K&Rは理想的だ。 

と肯定的な意見を書いている
ただし、これは2001年に書かれたモノで(22年前だ!)、ISO(国際標準化機構)で制定されたC99の登場からたった2年しか経っていない。当時はまだANSI CあるいはISO C90の実装の方が多かった時期だったわけだ(当然新仕様が出てすぐ足並みが揃うわけじゃない)。
2023年の現在、当時の意見と全く同じ事を表明するとして「K&R」をいまだ引き合いに出すかどうかは分からない。

※9: ただし、「反復」として再帰を学ぶか反復構文を学ぶか、と言うのはSchemeとANSI Common Lispでは立場が違う。と言うより言語仕様が違う。
Schemeは仕様上、「末尾再帰最適化」を要求するんで、何でも末尾再帰で書けるように誘導する。一方、ANSI Common Lispでは「末尾再帰最適化」は要求されていない。「末尾再帰最適化」する実装は多いが必須じゃないんで、結果、「再帰」そのものはANSI Common Lispではそれほど重要視されない。故に、ANSI Common Lispでは「再帰以外の豊富な反復構文」を学ぶ、言わば「フツーの言語」範疇の教授になり、再帰は必ずしも必須ではなくなる。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事