Arantium Maestum

プログラミング、囲碁、読書の話題

「Haskellっぽさ」という「関数型プログラミング」のイメージ

関数型プログラミングが主流になりつつある」という話が(またしても)ツイッターで出ていた。LINQやらReactやらを引き合いに出した英語記事が議論の引き金のようだ。その話題の一部として「そもそも何をもって関数型プログラミングと呼んでいるのだ?」という議論もあった(ただしかなり局所的かつ小規模な議論だったが)。

いわゆる関数型プログラミング言語とされているものをいくつか挙げるとHaskellOCamlScala、Elm、Clojureなどがある。

これらの共通項としては「関数が値として扱える(その結果として高階関数のイディオムが豊富)」「イミュータブルなデータがデフォルト」の二点ではないだろうか?実際個人的には「イミュータブルなデータを豊富な高階関数で扱う」というのが「関数型プログラミング」の定義だと考えている。

ただ、これだけだと拍子抜けというかいわゆる「関数型プログラミング」の高尚さ、数学的な難しさというイメージからかけ離れている。そういったイメージは主にHaskellから来ているのでは?と考えている。

「高尚な関数型プログラミング」のイメージの源泉としてのHaskellっぽさはなんだろう?と考えると

  • 静的型付け
  • 代数的データ型とパターンマッチ
  • 型推論
  • 型クラスによるアドホック多相
  • 圏論ベースの抽象化(モナド、モノイドなど)
  • より高次元な型理論の概念(高カインド型など)
  • 遅延評価

あたりだろう。Scalaなどでも「関数型プログラミングなライブラリ」と銘打たれるものはこういった機能をサポートするものが多いように思える。

特にモナド関数型プログラミングのイメージと密接に繋がっている印象で、ただし例えばML言語族ではHaskellほどは重要な概念ではない(言語機能的にモナドを多用するのがHaskellほど自然ではない、というのも一因か?型クラスによるアドホック多相が大きい)。

私はOCamlClojureが好きなので、その視点からみると上記の特徴はあくまで「Haskellっぽさ」でしかない。しかしこれらが「イミュータブルなデータを豊富な高階関数で扱う」という最低限な関数型プログラミングを実用的にするための追加機能として積み上がっている、という側面はあると思う。OCamlClojureはまた違うものを積み上げているだけだ(OCamlならモジュール、Clojureは基本的なデータ構造を中心に言語が組み上げられているところなど)。

Haskellっぽさ」は「関数型プログラミング」の定義ではない。しかし「関数型プログラミング」を実用的にするために積み上げる機能群としては、非常に精緻な一つの解ではあるのだと思う。だからHaskellを学ぶことは当然関数型プログラミングの一つの側面を深く知ることになって勉強になる。ただし複数ある解のうちの一つである、という視点も忘れてはいけない。

アラン・ケイとJOSS

前回に続きJOSSについて。今回は「オブジェクト指向」の名付け親、Smalltalkの設計者などで知られるアラン・ケイがJOSSに影響を受けたらしい、という話。

ACMインタビュー

私がJOSSという言語を意識し出したのはアラン・ケイの2004年のACMインタビューを読んだ時だ:

queue.acm.org

Let’s say the adoption of programming languages has very often been somewhat accidental, and the emphasis has very often been on how easy it is to implement the programming language rather than on its actual merits and features. For instance, Basic would never have surfaced because there was always a language better than Basic for that purpose. That language was Joss, which predated Basic and was beautiful. But Basic happened to be on a GE timesharing system that was done by Dartmouth, and when GE decided to franchise that, it started spreading Basic around just because it was there, not because it had any intrinsic merits whatsoever.

このインタビューでJOSSが出てくるのはこの部分だけで、BASICを腐してその対比として「すでに存在するより優れたもの」の例としてJOSSを出している。しかしそれにしても"and was beautiful"というのはすごい賛辞ではないか。ケイがJOSSに感銘を受けている様が見てとれ、この言語・システムがケイにどのような影響を与えたのか非常に興味を覚えた。

BBCインタビュー

調べてみると、1990年のBBCによるドキュメンタリー"The Machine that Changed the World"でのアラン・ケイインタビューがまとめられていた:

openvault.wgbh.org

このインタビューではJOSSについてかなり深く語っていた。

まず「タイム・シェア」という観点からJOSSが非常に優れていたという話:

Right, in fact the best one of these was a system called JOSS at RAND Corporation in which the system was devoted entirely to running this single language. That made things much better. The language was designed for end users. It was the first thing that effected people the way spreadsheets do today. It was designed -- it had eight users on this 1950s machine, but the guy who did it, Cliff Shaw, was a real artist, and the feeling of using this system was unlike that of using anything else on a computer. And people who used that system thought of wanting all of their computing to have the same kind of aesthetic, warm feeling that you had when using JOSS.

"real artist"、"aesthetic, warm feeling that you had when using JOSS"とやはり最高級に近い賛辞だ。

次にケイの1968年の修論にもなったFLEXについて:

Interviewer: WHAT WERE YOU TRYING TO DO WITH FLEX?

Kay: Well, I worked on this machine with a guy by the name of Ed Cheadle who was, he was trying to do, really trying to invent what today we would call personal computing. And he had a little machine and he had a little Sony television set, and what he wanted was something for engineers that would allow them to -- he was an engineer -- and he wanted something that allow them to flexibly do calculations beyond the kinds of things that you do with a calculator. ... And then I sort of came and corrupted the design by wanting it to be for people other than engineers. I'd seen JOSS, and I'd also recently seen one of the first object-oriented programming languages, and I realized how important that could be. ...

So the result of this, this, this machine was a technological success and a sociological disaster. And it was the, the magnitude of the rejection by non-computer people we tried it on, that got me thinking about user interface for the first time. And I realized that what Cliff Shaw had done in JOSS was not a luxury, but a necessity. And so it, it led to other, other ways of looking at things.

エンジニアではないエンドユーザに利用されるシステムを(JOSSを念頭に)作りたいと考えてやってみたものの、UIに関してユーザ目線を徹底しなかった結果技術的には成功だったものの使われずに終わり、JOSSのインタフェースへのこだわりが"not a luxury, but a necessity"だと悟った、という話。その後のケイの貢献を考えると、この観点を得るきっかけの一つがJOSSだったというのはとても面白い。

I had called the FLEX machine a personal computer. I think that was the first use of that term. While I was trying to figure out what was wrong with it I happened to visit RAND Corporation over here in Santa Monica and saw sort of a follow-on system to JOSS that they had done for their end users who were people like RAND economists. These people loved JOSS but they hated to type. And so, in the same year the mouse was invented, the RAND people had invented the first really good tablet.

JOSSの後継(この場合のJOSSとは言語ではなく対話的環境を含んだシステムすべてだろう)としてRANDでは非エンジニアなユーザの使用に耐えるタブレットとそのシステムを作り上げていた。"in the same year the mouse was invented"とあるので1968年だろうか。

最後にJOSSとLOGOの関係:

I saw Seymour Papert's early work with LOGO for, here were children writing programs and that happened because they had taken great care to try and combine the power of the computer with an easy to use language. In fact they used the RAND JOSS as a model and used the power of LISP which had been developed a few years before as a an artificial intelligence language, put them together and that was the early LOGO.

LOGOはturtle graphicsなどで有名なLISPベースの教育的コンピューティングシステム・言語で、インタプリタによる対話的環境を実現していた。ここでケイは明確に"they used the RAND JOSS as a model"と書いていて興味深い。ただしLOGOのウィキペディア記事Logo (programming language) - WikipediaではJOSSへの言及はない(Smalltalkへの影響についての言及はある)。こちらについてはもうちょっと調べてみたいところだ。

まとめ

ここまでみていて、アラン・ケイがJOSSとその開発者であるCliff Shawに対し非常に敬意を持っていることがわかる。また「ユーザインタフェースへの徹底的なこだわりによる非エンジニアへのコンピュータシステムの提供」というケイの思想的背景の源流の一つが(実際にRANDに行って触れていた)JOSSであるということも。そしてSmalltalkのインスピレーションの一つとして知られているLOGOの裏にもJOSSがある(あるいは少なくともケイはそう考えていた)。

アラン・ケイが現在使われているコンピュータ・システムに多大な影響を与えたことはよく知られている。JOSSがそのケイの源流の一つであるなら、その思想と業績を通じてJOSSも(人知れず?)コンピュータ史に非常に大きな貢献をした、と考えられるのではないだろうか。

JOSSについての覚え書き

JOSSという言語とプログラミング環境があった、らしい。

en.wikipedia.org

初出は1963年、LISPの5年後でBASICの1年前である。RANDコーポレーションのJOHNNIACマシンを数学者が対話的に扱うための言語とシステムで、開発者はCliff Shaw。以前ブログで取り上げたリスト処理やプログラミングにおける再帰の初出だと思われるInformation Processing Languageの開発者の一人である。

JOSSは最初期の対話的プログラミングシステムであり、インタラクティブなエディタで入力した行を逐次実行して結果を得ることができた。JOSSよりも先に存在していた対話的環境はあったのだろうか?LISPwikipediaのREPL記事によると1964年にREPLが開発されたということなのでJOSSの登場後となる。

よりアセンブリ言語に近いようなもので原始的な対話が可能だった環境はあってもおかしくない気はするが、モダンな意味での対話的環境としてはLISPやBASICより先だったのだからJOSSが初めてである可能性は充分ある。

もう一つJOSSが歴史的に面白いかもしれないポイントとしては、LETによって変数を定義する初めての言語である可能性があることだ。JOSSの言語仕様を読むと、変数定義にはSETという予約語が存在していて、LETは本来関数定義に使われるための予約語なのだが、仮引数を指定しない場合はLETもただの変数定義として使えたようだ。ここら辺のSETとLETの使い分けに関してはややこしいので集合知としてNewsletterで取り上げられた、とThe Joss Years: Reflections on an ExperimentというRANDコーポレーションの回顧録的な資料にある:

Problem areas thus discovered were often discussed for the benefit of all users in the JOSS Newsletter until its discontinuation in June 1971. Newsletter items differentiated between Set and Let, Do and To, and Done and Quit and Stop and Cancel.

もしもこのLETの機能がJOSSが出た1963年に存在したならば、LETの関数型プログラミングにおける初出である1965年のThe Next 700 Programming Languages・ISWIMは元より、プログラミング全体での初出だと私が考えていたDartmouth BASICの1964年よりも早いことになる。

ただ、Cliff Shawが1965年に書いたJOSS: Experience with an Experimental Computing Service for Users at Remote Typewriter ConsolesではLETが出てこない。初期のJOSSでは関数定義の機能がなかったのでは?という疑問があり、その場合1965年時点では変数定義の予約語はSETのみとなって、LETの導入は前述のISWIMやBASICより後になる。

ちなみにアラン・ケイがJOSSについて非常に好意的なコメントを残している。ケイがJOSSの対話的環境を参考にしていたなら、後世への影響としては非常に大きなものだと言える。この件についてはまた今度まとめたい。

短くブログを書く実験

気づいたらブログを更新しないまま6月が過ぎてしまった。

記事を書く上での自分の中の期待値とコストが増大していき、下書きで数千文字書いた時点で終わりが見えなくなってしまって放置する。そういうケースが3〜4本出てくると徒労感が募って手が出なくなる、という鉄板の流れがあり、今回も例に漏れずそのパターン。

論文の話をするのももう少し軽めにできるかと思ったのだが、コストのコントロールが難しい。実際のところ、人にある程度話せるレベルで論文を読み込むというのはエネルギーのかかることなのだ、というのは当たり前といえば当たり前なので仕方がないのかもしれない。数行で要約して一言感想を書くぐらいに留めるならツイッターでやった方が良さそうだ。ただ、ブログ記事を書くにあたって読み込んでわかったのは、自分が数行でまとめる場合の理解というのはかなり浅くて論文の芯を食っていないことも多いということ。記事に書く意義は非常に大きいのだけど、如何せん継続してそのコストを受け入れて記事を書いていくには断固たる意志が必要・・・。

というわけでこれから7月末までは、リハビリ的に本数重視で毎日更新、記事の長さはなるべく抑えめ、長くなりそうだったら部分的にまとめて、大体500〜1000文字の間で出していく、というやり方で書いていきたい。

終わった後に「これなら書きやすくていいな」と思うのか、「これでもそんな頻度では書けない」と思うのか(コスト的に、あるいはネタが枯渇して)、あるいは「こんな風に書いても仕方ない」と思うのか(自分が満足できるレベルで内容が練れていない、まったく書き足りない、など)。とりあえずは実験である。

読書メモ:The Rise of "Worse is Better"

逆説的でキャッチーなフレーズ「Worse is Better」の原点であるRichard GabrielのThe Rise of "Worse is Better"を再読したのでメモ。

概要

The Rise of "Worse is Better"は1989年にRichard Gabrielが書いた「Lisp: Good News, Bad News, How to Win Big」という商業Lisp業界のそれまでとその後の展望を語った文章の一章。1991年にLucid社員のJamie Zawinski(Coders at Workにも出てた)が発見してインターネットに放出して有名になったらしい。

LispC言語を対比させ、それらの開発母体であるMITとNew Jersey(ベル研究所の所在地)のローカルな哲学の違いが言語仕様を制定するさいの優先順位に影響を与え、結果それらの言語の成功の度合いも決定した、というのが文書の主張。そしてNew Jerseyの哲学を要約すると「Worse is Better」となる。

作者

作者であるRichard GabrielはLisp界隈の有名人。ただし一般的にはこの文書とWorse is Betterというフレーズが一番有名だろう。

他の功績としてはなんと言ってもCommon Lispの生みの親の一人であるという点。様々な団体が様々なアーキテクチャ向けに独自のLispを開発・販売していたのを、一つの規格へと統一するARPA主導のプロジェクトであったCommon Lisp。そのデザイナー五人衆(Quinquevirateと呼ばれていたらしい)にGuy Steele, David Moon, David Weinreb, Scott Fahlmaとともに名を連ねている。

またGNU EmacsのフォークであるXEmacsを開発していたLucid社の創業者でもある。この会社は元々Lisp関連の開発をしていたのが、その後他の事業にピボットしたという経緯がある(創業10年で倒産するのだが)。「Lisp: Good News, Bad News, How to Win Big」を書いた時点でのGabrielの肩書きはLucidのCEO。

詳細

この記事は話の流れとしては以下の4つに分かれる:

  • MITとNJの哲学をSimplicity, Correctness, Consistency, Completenessの四つの目的に対する優先順位の違いで解説
  • 実際のMITとNJのOS研究者の会話をアネクドートとして紹介
  • なぜNJ方式がソフトウェア戦略として優れているのか
  • なぜMIT方式は問題があるのか

MITとNJの哲学

MITの哲学は"the right thing"、NJの哲学は"Worse is Better"。

MIT哲学の優先順位はCorrectness = Consistency > Completeness > Simplicity (Interface) > Simplicity (Implementation)

NJの哲学はSimplicity (Implementation) > Simplicity (Interface) > Correctness > Completeness > Consistency

(注:Simplicity (Interface)の順位は原文からは不明瞭なので上記の不等式は推測も含まれる)

MITとNJのOS研究者の会話

割愛

なぜNJ方式がソフトウェア戦略として優れているのか

NJ方式だとうまくいく理由が三点挙げられている:

  • 実装のシンプルさが何よりも優先されるので、新しいアーキテクチャへのポートコストが低く、またリソース要求も低い(ので多くのプラットフォームに広まりやすい)
  • ユーザであるプログラマも実行効率とリソース効率を得るために安全性や利便性を犠牲にすることに慣れるので、NJ式の言語やOSの上で書かれるプログラムも広まりやすい
  • 複雑でモノリシックなプログラムを書けるほど表現力が強くないので、自然と再利用可能なコンポーネントを組み上げるスタイルの開発になる

必要と思われる機能の5割だけ実装されているとしても、上記の理由でウイルスのように広まりやすく、そして一旦広まってしまえば機能を9割まで上げる経済的なインセンティブが生じるので、最終的にはいいプロダクトになっていく。

なぜMIT方式は問題があるのか

MIT方式の結果は「big complex system」か「diamond-like jewel」のどちらかになる。

big complex systemの特徴は、大きく複雑で考え得る機能が網羅されている。デザイン、実装に非常に時間がかかり、利用するのに複雑なツールが必要となり、実行する環境に対するリソース要求が高い。例はCommon Lisp

diamond-like jewelの特徴はコンパクトなのだけど完璧にデザインするために開発にやはり非常に時間がかかること。また実行効率の良い実装はほぼ不可能。例はScheme

このような実装に時間がかかって、その上広まりにくいようなソフトが出来上がるthe right thingという哲学はソフトウェア開発においてはそぐわず、これはLisp業界にとって悩ましい問題である、という結論で終わっている。

感想

メッセージパッシングのちょうどよさのはなしを読んでいて、ひさしぶりにWorse is Betterへの言及を見たのでこれを機に読み直してみた。

以前はかなり感心した覚えがあるのだが、今回読み直してみて少し話の持っていきかたが強引な気もした。

プラットフォームがNJ式なことと、そのプラットフォーム上で書かれるプログラムがNJ式なのとは別問題じゃないか?あと実装が簡単だからといって低リソースな状況で強いかどうかもまた別では・・・?

まあDon't Let Perfect be the Enemy of Goodという話に関してはその通りだとは思うが、それ以外では前提条件がけっこう限定的な印象。

余談だがWorse is Betterが優れているのを説明する部分で

The good news is that in 1995 we will have a good operating system and programming language; the bad news is that they will be Unix and C++.

というフレーズが出てくるのだが

The good news is that in 2000 we will have a good operating system and programming language; the bad news is that they will be Windows and Java.

あるいは

The good news is that in 2015 we will have a good operating system and programming language; the bad news is that they will be Chrome and JavaScript.

という世界線から見直すとどうだろう。JavaJavaScriptも「Worse is Better」な思想かどうかは別としてウイルス的に爆発的に広がる理由があったのは事実。

Javaは巨大なVMでマシンアーキテクチャの違いを吸収して「Write once, run anywhere」を目指すという、ある意味「the right thing」な思想だったようにも思う(Guy Steeleが関わっていたという点も面白い)。「the right thing」でもスケールできるぞ、というアンサーソングだと妄想してみると楽しい。

JavaScriptはブラウザという爆発的に増えた生物の遺伝子コードにたまたま乗っかっていた、という感じだ。言語としては確かに「the right thing」を目指さなかった(時間的制約もあって目指せなかった)というところはあると思うけど、必ずしも「worse is better」を狙った感じではないし、「worse is better」だったから広まったわけでもなさそう。

PythonのNaN周りの挙動とIEEE754についてのメモ

こういうツイートがあった

「まあ大体IEEE 754のせい」でほぼ終わる話ではあるのだけど、少し深掘りもしたのでちょびっと備忘録メモ。

問題を(少し順番を変えた上で)再掲すると:

x = float('nan')
x == x # False
[x] == [x] # True

y = float('nan')
x == y # False
[x] == [y] # False
[x] == [copy.deepcopy(x)] # True

NaN != NaN

まず

x = float('nan')
x == x # False

に関して。

NaN != NaN浮動小数点数の国際規格であるIEEE 754で決まっている挙動。これがNaN周りの怪しげな挙動の諸悪の根源である。そもそも自己同一性が保たれていないので、直観的な挙動にならなくても仕方ない。

なんでそんな定義になっているのか、という話がStack Overflowに載っていた:

stackoverflow.com

回答者はIEEE 754の委員会メンバーだったとのこと。この挙動を決めた委員会メンバーであるKahanに直接聞いたところ、最大の理由は

there was no isnan( ) predicate at the time that NaN was formalized in the 8087 arithmetic; it was necessary to provide programmers with a convenient and efficient means of detecting NaN values that didn’t depend on programming languages providing something like isnan( ) which could take many years.

というわけで、この仕様が現実的にすぐに使われるためにはどうにかしてNaNチェックができるようにする必要があり、x != xはそのチェックのためのハックだったようだ。isnanがサポートされてしまった今となっては単なる歴史的経緯で、その結果として数十年後もこの微妙な挙動がサポートされ続けているので複雑な気持ちになる。

NaNの入ったリスト

ではなぜ

x == x # False
[x] == [x] # True

のような挙動になるのかというと、Pythonではリスト(というかcontainer)の比較は、要素の値を比較する前にポインタを比較するから。

この挙動に関しては2006年にバグトラッカーで少し議論があった模様:

bugs.python.org

All your example shows is that the oddball NaN is in-fact odd.

I do not want the rest of Python mucked-up just because NaNs are designed to not follow the most basic definitions of equality

とのこと。この判断はまあ妥当に思える。実行速度面から言っても、もし[NaN] != [NaN]を認めるならx = [NaN]; x != xも認める必要が生じ、その結果すべての等価性の判定でデータ構造のすべてを触ってNaNが含まれていないかを調べる必要が生じる。「もしかしたらNaNが含まれているかもしれない」という可能性のために、すべてのデータ構造の等価性チェックにおいて「ノードが同じオブジェクトである」とわかってもチェックを打ち切れなくなる、というのはあまりにも大きなコストだろう。

ちなみにRubyでもNaN入りのリストの挙動は同じようだ:

Rubyのバグトラッカーのチケット:

bugs.ruby-lang.org

こちらの方が様々なオプションを比較して議論していて面白い。結論は同様。

NaNをdeepcopy

だったら

y = float('nan')
x == y # False
[x] == [y] # False
[x] == [copy.deepcopy(x)] # True

という挙動はどういう理屈なのか。

x == yがFalseなのはIEEE 754から。

[x] == [y]がFalseなのはまずポインタが比較され、NaNはインターンされないのでメモリロケーションが違う、そして値の比較もIEEE 754準拠で当然False。

[x] == [copy.deepcopy(x)]で新しくコピーしているのだからポインタ比較はFalseであるべきでは?と思うかもしれないが、copy.deepcopyがコピーするのはミュータブルな構造のみでイミュータブルだと判定できるものに関してはコピーしない。id(x) == id(copy.deepcopy(x))なのでリスト比較はTrueを返す。これも当然イミュータブルな値であれば問題ない挙動なはずなのだが、NaN != NaNのせいで歪みが出る。

終わりに

Pythonバグトラッカーで"nan"と検索すると「minやmaxやsortの挙動がおかしい」などいろいろ面白い話題がある。

bugs.python.org

von Neumannを剽窃して

Any one who considers comparing IEEE 754 floating point numbers in the possible presence of NaNs is, of course, in a state of sin.

と言いたくなる。

Pythonで変数スコープがブロックベースだと大変そう

ツイッターPythonの文法の話題が盛ん(三角関数と同じで周期的に話題になる)で、個人的にはあまり同意できない意見も多いのだがなかなか面白いトピックもある。

例えば以下のようなコード:

fs = []
for i in range(5):
    fs.append(lambda: i)

for f in fs:
    print(f())

これが多くの人の直観に反して4を5回出力するという挙動になる。

このような挙動になる理由は複合的で:

  • 変数iのスコープがfor内のブロックで切れていない
  • lambda: iで定義された関数がクロージャとしてlexical scopeで変更され続ける変数iを捕捉している

の二点が関係している。

クロージャとlexical scopeの話はまた今度したいが、本記事ではPythonの変数スコープがブロックで切れない、という話についてつらつら考えてみたい。

Pythonの変数スコープの境界は大まかに言って関数とクラスである。より細やかな制御構文単位(例えばifやfor、try/exceptなど)ではスコープは切れない。

つまり関数内で変数に代入したとして:

x = 1
def f():
    x = 2
f()
# ここでもまだx=1

関数外の変数には変更はないが、例えば:

x = 1
if True:
  x = 2
# ここだとx = 2

ifブロックの中で変数に代入した場合、ブロック外の変数の値が変わっている。

この変数スコープのルールが把握しにくいということはないと思うし、個人的には殊更不自然な仕様だとも感じない。ただC言語族とは流儀がかなり違う、という点でそちらに慣れている人にとっては非直感的になる恐れはある。ちなみに関数が最小の変数スコープというのはPascalもそうだったらしく、Pythonが初出ということはなさそうだ。プログラミング言語史における変数スコープの変遷というのもなかなか面白そうなトピックである。

この仕様の初出がPythonではないとは言え、Pythonがこの変数スコープのルールを導入しているのには(多分)理由がある。Pythonの他の構文との兼ね合いから、ブロックレベルでのスコープは非常に相性が悪いのである。

例えば例外処理で

try:
    res = some_func_that_can_throw(my_args)
except SomeError:
    some_cleanup()
    res = default_value

のようなコードをよく書くと思うのだが、これはresのスコープがブロックのみだとまったく意味をなさない。まあこれはC++などでも同じで、じゃあresがそもそもtryの外で定義されていればいいのでは?と

res = default_value
try:
    res = some_func_that_can_throw(my_args)
except SomeError:
    some_cleanup()

のように変更したとしても、今度はtryの中のresはどのスコープのものになるのか?という問題が生じる。つまりPythonには変数代入と独立した変数宣言が存在しないので、ブロックの中の変数がこのブロックの中で新たに作られるべき変数なのか、それとも外部スコープにすでに存在する変数を意図しているのか、判定するのが難しい。

同じ変数がスコープ外に存在しているならそれを使い、存在しないなら現在のスコープに限定された変数を作る、というような挙動もありえるだろうが、こちらの方が変数のスコープが意図しない形になってしまう可能性は高そうだ。あるいは関数の構文を真似てnonlocalキーワードで外部スコープの変数を使うのを明示するという手もあるが、非常に冗長になってしまう。(関数の場合、外部スコープの変数を書き換えるというのはかなり例外的なのに対して、制御構文内ではほとんどのケースでこのような書き換えが必要になる)

なのでJSのようにletなどで代入とは別に変数宣言できるようにしていない場合、制御構文のブロックベースで変数スコープを切ってしまうのは不都合。

「だから変数宣言が別で必要だ」と考えることもできるのだけど、実際のところ「関数が変数スコープの最小単位」というメンタルモデルを持ってしまえば正直これが不便だと感じることもほとんどない、というのが個人的な感想。基本的に「同一関数の中で同じ変数を別のことに利用しない」というのはさほど不自然なルールではないし、Pythonの文化的にもなるべく関数は小さく、ネストは浅く、という記法が好まれているので(まあこれは汎用的なスタイルだとは思うが、CやC++より守ろうと頑張る傾向があるように思う)同一変数を別のことに使いたくなってきたら別関数に切り出す事になる。

なので正直「変数スコープがブロックではなく関数単位」というのは「変数宣言を言語に導入する」レベルで言語仕様を変える必要のあるような問題(というかそもそも解決するべき「問題」)だとは思えない。

ただし、forブロックから変数が「漏れる」ということと、ブロック内で作ったクロージャが値ではなくlexical scopeの変数そのものを捕捉してしまうことの合わせ技である、本記事冒頭の非直感的挙動は確かにあまり嬉しくない。

解決策としては

fs = []
for i in range(5):
    fs.append(lambda x=i: x)

でデフォルト引数として保存させるのが一般的。あるいは

fs = []
for i in range(5):
    fs.append((lambda x: lambda: x)(i))

と新しくlexical scopeを作ってやる手もある。どちらもちょっとイケテナイ感が漂うのは事実だが・・・

次回はクロージャとLexical scopeの話。