17
10

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-07-31

変更の概要

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では、globallocalキーワードを使って、ある変数がグローバルかローカルかを明示することができます。もしそれらのキーワードがない場合、Juliaは、あるルールで変数のグローバル・ローカルを判定します。その判定方法(Julia 1.0から1.4まで、1.5以降のREPL以外)は、開発者のひとりであるStefan Karpinskiが簡潔にまとめています

  • ローカルスコープのブロックは、関数、ループ、trystruct、内包表記(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開発者自らスレッドを立ち上げ(たとえば12)、その改善策が議論されてきました。

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

17
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
10