Julia 1.5以降のREPLにおける変数スコープの扱い

# 変更の概要

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
# 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
# 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
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が[簡潔にまとめています](https://discourse.julialang.org/t/explain-scoping-confusion-to-a-programming-beginner/43206/7)。

- ローカルスコープのブロックは、関数、ループ、`try`、`struct`、内包表記（comprehensions ）が作る。
- あるローカルスコープ内に、変数への代入 `x = ...` が存在するとき：
    - もし `x` がすでにローカル変数であると確定しているなら、それに代入する。
    - それ以外なら、新しいローカル変数 `x` を作る。

補足するなら、ローカルブロック以外で定義された変数は、すべてグローバル変数になります。REPLやスクリプトに直接打ち込んだ変数がそれに該当します。より詳しい説明は、[@yatra9さんの記事](https://qiita.com/yatra9/items/d38833b8d4f30b921f52)、[@NamaNamazuさんの記事](https://qiita.com/NamaNamazu/items/6fcd628d5bb8dd9621f6)または[丸井綜研さんのブログ](https://marui.hatenablog.com/entry/2019/08/09/115410)にあります。

これらのルールは、グローバル変数を多用する場合（REPLでの利用の場合）には利便性を低下させます。また、ある関数の一部をREPLにコピー＆ペーストしても、まともに動かないかもしれません。一方、グローバル変数の予期せぬ上書きを予防できるメリットもあります。前述のStefan Karpinskiは、[同じスレッドの別の投稿](https://discourse.julialang.org/t/explain-scoping-confusion-to-a-programming-beginner/43206/22)で、安全でシンプルな解決策としてこのスコープルールが決定されたと述べています。かつて、Juliaチームが利用していたスクリプトにはグローバル変数の予期せぬ変更が相当数存在しており、彼らをもってしても見逃すことがあり、バグの温床であったようです。

## Julia 1.5以降のREPLのスコープ

新しいルールは、Julia 0.6以前で採用されていたものです。当時、ローカルスコープにはソフト（soft）とハード（hard）の2つがあり、関数と`struct`はハード、それ以外はソフトローカルスコープを定義しました。[当時のマニュアル](https://hshindo.github.io/julia-doc-ja-v0.6/manual/variables-and-scoping.html)によると「ソフトローカルスコープでは、わざわざlocalキーワードをつけていない限り、すべての変数は親スコープから受け継がれます」とあります。そして、受け継がれた変数は、読み取りと変更の両方で利用できます。一方、ハードローカルスコープの説明には「グローバル変数は、読取だけが受け継がれ、書込は受け継がれません」とあります。これはJuliaの標準的なスコープと同じ挙動です。

Julia 1.5でなされた変更は、REPLのローカルスコープにソフトとハードの2つを再導入するものです。前述のように、スクリプトを`include`する場合には、このルールは適用されません。スクリプト内のループでグローバル変数を変更するには、明示的に`global`キーワードを挿入します。

## メモ: Juliaにおけるスコープの議論

REPLかファイルかに関係なく、一律でグローバル変数へのアクセスを制限する[修正](https://github.com/JuliaLang/julia/pull/19324)は、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](https://github.com/JuliaLang/julia/issues/28789)が立ち、議論が始まりました。Github、Stack Overflow、公式Discourseには、定期的に、これに関する質問が寄せられました。以前から懸念を示していたSteven Johnson（IJuliaやFFTWの作者）は、REPL上での挙動を以前のものに戻すことを強く主張し、自らのメンテナンスするIJuliaが以前と同じように動作するように修正しています。また、Julia開発者自らスレッドを立ち上げ（たとえば[1](https://discourse.julialang.org/t/another-possible-solution-to-the-global-scope-debacle/15894)や[2](https://discourse.julialang.org/t/new-scope-solution/16707)）、その改善策が議論されてきました。

当初、Julia 1.1までに修正してはどうかとの要望が多く寄せられましたが、これは困難でした。Julia 1.0がリリースされた直後で、大きなbreaking changeを採用できなかったことに加え、スコープのルールを簡潔に保つ必要があったため、議論に時間がかかりました。おおよそ考えつく限りの改善案が出され、いくつかはプロトタイプが作成されて試験されました。最終的に、開発者側からREPLのみJulia 0.6相当の挙動に戻す[pull request](https://github.com/JuliaLang/julia/pull/33864)が出され、数ヶ月の議論の後に取り込まれました。