変更の概要
2020年10月4日追記:もともとこの記事は、Julia 1.5がリリースされる直前に書かれました。当初の内容は、Julia 1.4以前の視点で、新バージョンの挙動を説明するものでした。この追記では視点を変え、Julia 1.5以降の利用者が、旧バージョンの挙動を振り返るようにしました。また、このスコープルールの決定に至るまでの経緯をまとめました。
- Julia 1.5以降、REPL上の変数スコープの挙動が変更になった。
- Julia 1.4までのデフォルトの挙動は「ローカルスコープ内からグローバル変数を参照できるが変更できない」とされていた。
- 1.5以降は、ループなどの一部のローカルスコープ内で、グローバル変数の参照と変更が(デフォルトで)可能になった。結果、REPLでループを書く場合に、他の言語と似た感覚で変数を変更できるようになった。
- この変更は、JuliaのREPL上でプログラムを直接入力する場合にのみ影響する。
- たとえばスクリプトを
include
する場合など、REPLに直接入力する場合以外には適用されない(Julia 1.4までの挙動が維持される)。 - また、IJuliaは、当初からこの新挙動のみをサポートしてきた。したがって、今回の変更はIJuliaには影響しない。
- たとえばスクリプトを
新旧挙動の比較
Julia 1.0から1.4までのバージョンには、直感的に理解し難い挙動がありました。その典型的なものが、変数のスコープでした。これは、REPL上でループを使うことで、簡単に遭遇しました。
# Julia 1.0 から 1.4まで
julia> s = 0
julia> for i in 1:10
s = s + 1
end
ERROR: UndefVarError: s not defined
Stacktrace:
[1] top-level scope at .\REPL[2]:2
上記のs
は、REPL上ではグローバル変数として扱われました。さらにJuliaでは、for
はローカルスコープを定義し、その内部からはグローバル変数は(読み出しはできても)変更できない仕組みになっていました。もしfor
内でs
を変更したければ、global s=s+1
としなければなりませんでした。
このグローバル変数に関する挙動は意図されたもので、慎重な議論の上で選択されたデザインでした。しかし、これはあまりに非直感的であり、他のプログラミング言語の経験者を混乱させる結果となりました。特に、教育関係者から、(コンピュータサイエンス以外の)授業でループを使う際に大きな障害となるという懸念が示されました。さらに、Juliaを使い慣れた開発者からも不便だという意見が挙がりました。コピー&ペーストでREPLにコードを貼り付けても、スコープの問題で、それが動作するとは限らなかったからです。
そこで、Julia 1.5からは、REPL上での変数スコープが変更され、上のコードがより直感的に動作するようになりました。なお、IJulia(Jupyter notebook)では、この新しい挙動をJulia 1.0以前から採用してきました。したがって、IJuliaを使う限り、この問題は顕在化しませんでした。
# Julia 1.5以降
julia> s = 0
julia> for i in 1:10
s = s + 1
end
julia> s
10
Julia 1.5以降では、この新しい挙動は、REPL上でコマンドを直接入力する場合に限り有効です。ですので、これらのコマンドをスクリプトに書き込み、include
で読みこむと、従来(Julia 1.4以前)と同じルールが適用になって警告が現れ、エラーで停止します。
julia> include("run.jl")
┌ Warning: Assignment to `s` in soft scope is ambiguous because a global variable by the same name exists: `s` will be treated as a new local. Disambiguate by using `local s` to suppress this warning or `global s` to assign to the existing global variable.
└ @ D:\downloads\software\math\run.jl:3
ERROR: LoadError: UndefVarError: s not defined
実は、この新しい挙動はJulia 0.6以前で採用されていたものです。つまり、REPLに限っては、昔のルールのほうが直感的に利用できるという結論に至ったのですね。
Juliaのスコープについて
標準ルール
Juliaでは、global
とlocal
キーワードを使って、ある変数がグローバルかローカルかを明示することができます。もしそれらのキーワードがない場合、Juliaは、あるルールで変数のグローバル・ローカルを判定します。その判定方法(Julia 1.0から1.4まで、1.5以降のREPL以外)は、開発者のひとりであるStefan Karpinskiが簡潔にまとめています。
- ローカルスコープのブロックは、関数、ループ、
try
、struct
、内包表記(comprehensions )が作る。 - あるローカルスコープ内に、変数への代入
x = ...
が存在するとき:- もし
x
がすでにローカル変数であると確定しているなら、それに代入する。 - それ以外なら、新しいローカル変数
x
を作る。
- もし
補足するなら、ローカルブロック以外で定義された変数は、すべてグローバル変数になります。REPLやスクリプトに直接打ち込んだ変数がそれに該当します。より詳しい説明は、@yatra9さんの記事、@NamaNamazuさんの記事または丸井綜研さんのブログにあります。
これらのルールは、グローバル変数を多用する場合(REPLでの利用の場合)には利便性を低下させます。また、ある関数の一部をREPLにコピー&ペーストしても、まともに動かないかもしれません。一方、グローバル変数の予期せぬ上書きを予防できるメリットもあります。前述のStefan Karpinskiは、同じスレッドの別の投稿で、安全でシンプルな解決策としてこのスコープルールが決定されたと述べています。かつて、Juliaチームが利用していたスクリプトにはグローバル変数の予期せぬ変更が相当数存在しており、彼らをもってしても見逃すことがあり、バグの温床であったようです。
Julia 1.5以降のREPLのスコープ
新しいルールは、Julia 0.6以前で採用されていたものです。当時、ローカルスコープにはソフト(soft)とハード(hard)の2つがあり、関数とstruct
はハード、それ以外はソフトローカルスコープを定義しました。当時のマニュアルによると「ソフトローカルスコープでは、わざわざlocalキーワードをつけていない限り、すべての変数は親スコープから受け継がれます」とあります。そして、受け継がれた変数は、読み取りと変更の両方で利用できます。一方、ハードローカルスコープの説明には「グローバル変数は、読取だけが受け継がれ、書込は受け継がれません」とあります。これはJuliaの標準的なスコープと同じ挙動です。
Julia 1.5でなされた変更は、REPLのローカルスコープにソフトとハードの2つを再導入するものです。前述のように、スクリプトをinclude
する場合には、このルールは適用されません。スクリプト内のループでグローバル変数を変更するには、明示的にglobal
キーワードを挿入します。
メモ: Juliaにおけるスコープの議論
REPLかファイルかに関係なく、一律でグローバル変数へのアクセスを制限する修正は、Julia 0.6のリリース後(Julia 1.0以前)に提案されました。これはローカルスコープのソフト・ハードの区別を廃止するものでした。その結果、上で述べた標準ルールが常に適用されることを意味しました。
この時点でも、スクリプティング時の不便さへの懸念がいくつか挙がっています。ただし、スコープルールの単純化に加え、グローバル変数の意図せぬ上書きを防止できるメリットが大きく、このpull requestは当時のmasterに取り込まれました(2017年10月)。この修正はnightlyビルドに入っていましたが、多くの利用者はこの修正に反対しませんでした。パッケージ開発者は自力で不都合を回避できたことに加え、多くのユーザはnightlyを使わなかったので問題が発覚しなかったようです。
この修正はJulia 0.7(2018年8月)に含められ、その直後に出た1.0にも含まれました。多くのユーザは、この時点で初めてこの問題に遭遇し、直感的でない動作に頭を抱えました。Julia 1.0リリース直後から、GithubにはIssueが立ち、議論が始まりました。Github、Stack Overflow、公式Discourseには、定期的に、これに関する質問が寄せられました。以前から懸念を示していたSteven Johnson(IJuliaやFFTWの作者)は、REPL上での挙動を以前のものに戻すことを強く主張し、自らのメンテナンスするIJuliaが以前と同じように動作するように修正しています。また、Julia開発者自らスレッドを立ち上げ(たとえば1や2)、その改善策が議論されてきました。
当初、Julia 1.1までに修正してはどうかとの要望が多く寄せられましたが、これは困難でした。Julia 1.0がリリースされた直後で、大きなbreaking changeを採用できなかったことに加え、スコープのルールを簡潔に保つ必要があったため、議論に時間がかかりました。おおよそ考えつく限りの改善案が出され、いくつかはプロトタイプが作成されて試験されました。最終的に、開発者側からREPLのみJulia 0.6相当の挙動に戻すpull requestが出され、数ヶ月の議論の後に取り込まれました。