Область видимости переменных
Область видимости переменной — это область кода, в которой переменная доступна. Область видимости переменных помогает избежать конфликтов имён переменных. Концепция интуитивна: две функции могут иметь аргументы, названные x, без того, чтобы эти два x ссылались на одно и то же. Аналогично, существует множество других случаев, когда разные блоки кода могут использовать одно и то же имя, не ссылаясь на одну и ту же переменную. Правила, определяющие, когда одно и то же имя переменной ссылается на одну и ту же переменную, а когда нет, называются правилами области видимости; в этом разделе они подробно изложены.
Некоторые конструкции языка вводят блоки области видимости, которые являются областями кода, которые могут быть областью видимости набора переменных. Область видимости переменной не может быть произвольным набором строк исходного кода; вместо этого она всегда соответствует одному из этих блоков. Существует два основных типа областей видимости в Julia: глобальная область видимости и локальная область видимости. Последняя может быть вложенной. В Julia также есть различие между конструкциями, которые вводят «жёсткую область видимости», и теми, которые вводят только «мягкую область видимости», что влияет на то, разрешено ли затенение глобальной переменной переменной с таким же именем.
Конструкции области видимости
Конструкции, вводящие блоки области видимости:
| Конструкция | Тип области видимости | Разрешено внутри |
|---|---|---|
module, baremodule
|
глобальная | глобальная |
struct |
локальная (мягкая) | глобальная |
for, while, try
|
локальная (мягкая) | глобальная или локальная |
macro |
локальная (жёсткая) | глобальная |
let, функции, выражения с пониманием, генераторы |
локальная (жёсткая) | глобальная или локальная |
В этой таблице отсутствуют блоки begin и блоки if, которые не вводят новые области видимости. Три типа областей видимости следуют несколько разным правилам, которые будут объяснены ниже.
Julia использует лексическую область видимости, что означает, что область видимости функции не наследуется от области видимости вызывающей функции, а от области видимости, в которой функция была определена. Например, в следующем коде x внутри foo относится к x в глобальной области видимости её модуля Bar,
julia> module Bar
x = 1
foo() = x
end;
а не к x в области видимости, где foo используется:
julia> import .Bar julia> x = -1; julia> Bar.foo() 1
Таким образом, лексическая область видимости означает, что к чему относится переменная в определённом фрагменте кода, можно вывести из самого кода, в котором она появляется, и это не зависит от того, как выполняется программа. Область видимости, вложенная внутрь другой области видимости, может «видеть» переменные во всех внешних областях видимости, в которых она содержится. Внешние области видимости, с другой стороны, не могут видеть переменные во внутренних областях видимости.
Глобальная область видимости
Каждый модуль вводит новую глобальную область видимости, отличную от глобальной области видимости всех других модулей — нет всеобъемлющей глобальной области видимости. Модули могут вводить переменные других модулей в свою область видимости с помощью инструкций using или import или через квалифицированный доступ с использованием нотации точек, т.е. каждый модуль является так называемым пространством имён, а также структурой данных первого класса, которая связывает имена со значениями. Обратите внимание, что, хотя переменные могут считываться извне, они могут быть изменены только внутри модуля, к которому они принадлежат. В качестве выхода вы всегда можете оценить код внутри этого модуля для изменения переменной; это гарантирует, в частности, что привязки модулей не могут быть изменены извне кодом, который никогда не вызывает eval.
julia> module A
a = 1 # a global in A's scope
end;
julia> module B
module C
c = 2
end
b = C.c # can access the namespace of a nested global scope
# through a qualified access
import ..A # makes module A available
d = A.a
end;
julia> module D
b = a # errors as D's global scope is separate from A's
end;
ERROR: UndefVarError: a not defined
julia> module E
import ..A # make module A available
A.a = 2 # throws below error
end;
ERROR: cannot assign variables in other modules
Обратите внимание, что интерактивный пригласительный символ (также известный как REPL) находится в глобальной области видимости модуля Main.
Локальная область видимости
Новая локальная область видимости вводится большинством блоков кода (см. вышеприведённую таблицу таблицу для полного списка). Некоторые языки программирования требуют явного объявления новых переменных перед их использованием. Явное объявление работает и в Julia: в любой локальной области видимости запись local x объявляет новую локальную переменную в этой области видимости, независимо от того, существует ли переменная с именем x во внешней области видимости или нет. Объявление каждой новой локальной переменной таким образом несколько громоздко и утомительно, однако Julia, как и многие другие языки, считает присвоение новой переменной в локальной области видимости неявным объявлением этой переменной как новой локальной. В основном это довольно интуитивно, но, как и во многих вещах, которые ведут себя интуитивно, детали более тонкие, чем можно было бы наивно предположить.
Когда x = <value> происходит в локальной области видимости, Julia применяет следующие правила для определения значения выражения на основе того, где выражение присваивания встречается и к чему x уже относится в этом месте:
-
Существующая локальная переменная: Если
xуже является локальной переменной, тогда существующей локальной переменнойxприсваивается значение; -
Жёсткая область видимости: Если
xещё не является локальной переменной и присваивание происходит внутри любой конструкции жёсткой области видимости (т.е. внутри блока let, тела функции или макроса, выражения с пониманием или генератора), создаётся новая локальная переменная с именемxв области видимости присваивания; -
Мягкая область видимости: Если
xещё не является локальной переменной и все конструкции области видимости, содержащие присваивание, являются мягкими областями видимости (циклы,try/catchблоки илиstructблоки), поведение зависит от того, определена ли глобальная переменнаяx:- если глобальная переменная
xне определена, создаётся новая локальная переменная с именемxв области видимости присваивания; - если глобальная переменная
xопределена, присваивание считается неоднозначным:- в неинтерактивных контекстах (файлы, eval) выводится предупреждение об неоднозначности, и создаётся новая локальная переменная;
- в интерактивных контекстах (REPL, блокноты) присваивается значение глобальной переменной
x.
- если глобальная переменная
Вы можете заметить, что в неинтерактивных контекстах поведение жёсткой и мягкой областей видимости идентично, за исключением того, что выводится предупреждение при затенении глобальной переменной неявно локальной переменной (т.е. не объявленной с помощью local x). В интерактивных контекстах правила следуют более сложной эвристике для удобства. Это подробно рассматривается в последующих примерах.
Теперь, когда вы знаете правила, давайте рассмотрим некоторые примеры. Предполагается, что каждый пример оценивается в новой сессии REPL, чтобы единственными глобальными переменными в каждом фрагменте кода были те, которые присваиваются в этом блоке кода.
Начнём с приятной и ясной ситуации — присвоение внутри жёсткой области видимости, в данном случае тела функции, когда локальной переменной с таким именем ещё не существует:
julia> function greet()
x = "hello" # new local
println(x)
end
greet (generic function with 1 method)
julia> greet()
hello
julia> x # global
ERROR: UndefVarError: x not defined
Внутри функции greet присваивание x = "hello" приводит к тому, что x становится новой локальной переменной в области видимости функции. Существуют два важных факта: присваивание происходит в локальной области видимости, и нет существующей локальной переменной x. Поскольку x является локальной, не имеет значения, существует ли глобальная переменная с именем x или нет. Например, здесь мы определяем x = 123 перед определением и вызовом greet:
julia> x = 123 # global
123
julia> function greet()
x = "hello" # new local
println(x)
end
greet (generic function with 1 method)
julia> greet()
hello
julia> x # global
123
Поскольку x в greet является локальной, значение (или отсутствие такового) глобальной переменной x не изменяется при вызове greet. Правило жёсткой области видимости не учитывает, существует ли глобальная переменная с именем x или нет: присвоение x в жёсткой области видимости является локальным (если x не объявлено глобальным).
Следующая ясная ситуация, которую мы рассмотрим, — это когда уже существует локальная переменная с именем x, в этом случае x = <value> всегда присваивает значение этой существующей локальной переменной x. Функция sum_to вычисляет сумму чисел от единицы до n:
function sum_to(n)
s = 0 # new local
for i = 1:n
s = s + i # assign existing local
end
return s # same local
end
Как и в предыдущем примере, первое присваивание s в начале sum_to приводит к тому, что s становится новой локальной переменной в теле функции. Цикл for имеет свою собственную внутреннюю локальную область видимости внутри области видимости функции. В момент, когда выполняется s = s + i, s уже является локальной переменной, поэтому присвоение обновляет существующую локальную переменную s, а не создаёт новую. Мы можем проверить это, вызвав sum_to в REPL:
julia> function sum_to(n)
s = 0 # new local
for i = 1:n
s = s + i # assign existing local
end
return s # same local
end
sum_to (generic function with 1 method)
julia> sum_to(10)
55
julia> s # global
ERROR: UndefVarError: s not defined
Поскольку s локальна для функции sum_to, вызов функции не оказывает влияния на глобальную переменную s. Мы также можем видеть, что обновление s = s + i в цикле for должно было обновить ту же переменную s, созданную инициализацией s = 0, так как мы получаем правильную сумму 55 для целых чисел от 1 до 10.
Давайте углубимся в тот факт, что тело цикла for имеет свою собственную область видимости, написав немного более подробную вариацию, которую мы назовём sum_to′, в которой мы сохраняем сумму s + i в переменной t перед обновлением s:
julia> function sum_to′(n)
s = 0 # new local
for i = 1:n
t = s + i # new local `t`
s = t # assign existing local `s`
end
return s, @isdefined(t)
end
sum_to′ (generic function with 1 method)
julia> sum_to′(10)
(55, false)
Этот вариант возвращает s как и прежде, но также использует макрос @isdefined, чтобы вернуть булево значение, указывающее, существует ли локальная переменная с именем t в самом внешнем локальном объёме функции. Как вы можете видеть, t не определена вне цикла for. Это происходит из-за правила жёсткой области видимости: поскольку присвоение t происходит внутри функции, которая вводит жёсткую область видимости, присвоение делает t новой локальной переменной в локальной области видимости, в которой она появляется, то есть внутри тела цикла. Даже если бы существовала глобальная переменная с именем t, это не имело бы значения — правило жёсткой области видимости не зависит ни от чего в глобальной области видимости.
Перейдём к более неоднозначным случаям, охватываемым правилом мягкой области видимости. Мы рассмотрим это, извлекая тела функций greet и sum_to′ в контексты мягкой области видимости. Сначала поместим тело функции greet в цикл for — который является мягким, а не жёстким — и оценим его в REPL:
julia> for i = 1:3
x = "hello" # new local
println(x)
end
hello
hello
hello
julia> x
ERROR: UndefVarError: x not defined
Поскольку глобальная переменная x не определена при оценке цикла for, применяется первое условие правила мягкой области видимости, и x создаётся как локальная переменная цикла for, и поэтому глобальная переменная x остаётся неопределённой после выполнения цикла. Далее рассмотрим тело функции sum_to′ в глобальной области видимости, зафиксировав её аргумент в n = 10
s = 0
for i = 1:10
t = s + i
s = t
end
s
@isdefined(t)
Что делает этот код? Наводящий вопрос: ответ "зависит". Если этот код вводится интерактивно, он ведёт себя так же, как и внутри тела функции. Но если код находится в файле, он выводит предупреждение об неоднозначности и генерирует ошибку неопределённой переменной. Посмотрим, как он работает в REPL:
julia> s = 0 # global
0
julia> for i = 1:10
t = s + i # new local `t`
s = t # assign global `s`
end
julia> s # global
55
julia> @isdefined(t) # global
false
REPL приближает работу внутри тела функции, определяя, производит ли присвоение внутри цикла присвоение глобальной переменной или создаёт новую локальную переменную, в зависимости от того, определена ли глобальная переменная с таким именем или нет. Если глобальная переменная с таким именем существует, то присвоение обновляет её. Если глобальная переменная с таким именем не существует, то присвоение создаёт новую локальную переменную. В этом примере мы видим оба случая в действии:
- Глобальная переменная с именем
tне существует, поэтомуt = s + iсоздаёт новую переменнуюt, которая является локальной для циклаfor; - Глобальная переменная с именем
sсуществует, поэтомуs = tприсваивает ей значение.
Второй факт объясняет, почему выполнение цикла изменяет глобальное значение s, а первый факт объясняет, почему t всё ещё неопределена после выполнения цикла. Теперь попробуем оценить тот же код, как если бы он был в файле:
julia> code = """
s = 0 # global
for i = 1:10
t = s + i # new local `t`
s = t # new local `s` with warning
end
s, # global
@isdefined(t) # global
""";
julia> include_string(Main, code)
┌ 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.
└ @ string:4
ERROR: LoadError: UndefVarError: s not defined
Здесь мы используем include_string, чтобы оценить code как содержимое файла. Мы также можем сохранить code в файл и затем вызвать include на этом файле — результат будет тем же. Как вы можете видеть, это поведение сильно отличается от оценки того же кода в REPL. Давайте разберём, что происходит:
- Глобальная переменная
sопределена со значением0перед оценкой цикла; - Присвоение
s = tпроисходит в мягкой области видимости — циклеforвне любого тела функции или другой конструкции жёсткой области видимости; - Поэтому применяется второе условие правила мягкой области видимости, присвоение неоднозначно, поэтому генерируется предупреждение;
- Выполнение продолжается, делая
sлокальной для тела циклаfor; - Поскольку
sявляется локальной для циклаfor, она неопределена при оценкеt = s + i, что вызывает ошибку; - Вычисление останавливается там, но если бы дошло до
sи@isdefined(t), оно вернуло бы0иfalse.
Это демонстрирует важные аспекты области видимости: в области видимости каждая переменная может иметь только одно значение, и это значение определяется независимо от порядка выражений. Наличие выражения s = t в цикле делает s локальной для цикла, что означает, что она также локальная, когда она появляется в правой части t = s + i, даже если это выражение появляется первым и оценивается первым. Можно предположить, что s на первой строке цикла может быть глобальной, а s на второй строке цикла — локальной, но это невозможно, так как две строки находятся в одном блоке области видимости, и каждая переменная может иметь только одно значение в данной области видимости.
О мягкой области видимости
Мы рассмотрели все правила локальной области видимости, но перед завершением этого раздела, возможно, стоит сказать несколько слов о том, почему неоднозначный случай мягкой области видимости обрабатывается по-разному в интерактивном и неинтерактивном контекстах. Возникают два очевидных вопроса:
- Почему это не работает так же, как в REPL во всех случаях?
- Почему это не работает так же, как в файлах во всех случаях? И может быть, пропустить предупреждение?
В Julia ≤ 0.6 все глобальные области видимости работали так же, как текущий REPL: когда x = <value> происходило в цикле (или try/catch, или в теле struct) но вне тела функции (или блока let или вычисления), решалось, должна ли x быть локальной для цикла, основываясь на том, определена ли глобальная переменная с именем x или нет. Это поведение имеет преимущество интуитивности и удобства, поскольку оно максимально приближено к поведению внутри тела функции. В частности, это облегчает перемещение кода между телом функции и REPL при отладке поведения функции. Однако у него есть некоторые недостатки. Во-первых, это довольно сложное поведение: многие люди на протяжении многих лет путались в этом поведении и жаловались, что оно сложное и трудно объяснить и понять. Согласен. Во-вторых, и, возможно, ещё хуже, это плохо для программирования «в масштабе». Когда вы видите небольшой фрагмент кода в одном месте, как в этом случае, совершенно ясно, что происходит:
s = 0
for i = 1:10
s += i
end
Очевидно, цель — изменить существующую глобальную переменную s. Что ещё это может означать? Однако не весь реальный код настолько короткий и ясный. Мы обнаружили, что в реальном коде часто встречается следующий код:
x = 123
# much later
# maybe in a different file
for i = 1:10
x = "hello"
println(x)
end
# much later
# maybe in yet another file
# or maybe back in the first one where `x = 123`
y = x + 234
Гораздо менее ясно, что должно произойти здесь. Так как x + "hello" — ошибка метода, похоже, что цель — сделать x локальной для цикла for. Но значения времени выполнения и то, какие методы существуют, нельзя использовать для определения областей видимости переменных. С поведением Julia ≤ 0.6 особенно тревожит, что кто-то мог написать цикл for первым, и он работал хорошо, но позже, когда кто-то другой добавит новую глобальную переменную далеко — возможно, в другом файле — код внезапно изменит смысл и либо вызовет ошибку, либо, что ещё хуже, тихо сделает неверную работу. Этот вид «странного действия на расстоянии» — то, чего должны избегать хорошие языки программирования.
Итак, в Julia 1.0 мы упростили правила области видимости: в любой локальной области видимости присвоение имени, которое не было локальной переменной, создавало новую локальную переменную. Это полностью устранило понятие мягкой области видимости и устранило возможность «странного действия на расстоянии». Мы выявили и исправили значительное количество ошибок из-за устранения мягкой области видимости, подтверждая выбор избавиться от неё. И все радовались! Ну, нет, не совсем. Потому что некоторые люди были недовольны тем, что им теперь нужно писать:
s = 0
for i = 1:10
global s += i
end
Вы видите аннотацию global в ней? Отвратительно. Очевидно, такое положение дел нельзя было терпеть. Но серьёзно, есть две основные проблемы, связанные с требованием аннотации global для такого кода верхнего уровня:
Больше не удобно копировать и вставлять код из тела функции в REPL для отладки — нужно добавить аннотации
globalи затем снова удалить их, чтобы вернуться обратно;Начинающие программисты напишут такой код без аннотации
globalи не будут знать, почему их код не работает — ошибка, которую они получают, заключается в том, чтоsне определена, что, похоже, не проясняет ситуацию для тех, кто делает эту ошибку.
Начиная с Julia 1.5, этот код работает без аннотации global в интерактивных контекстах, таких как REPL или Jupyter-блокноты (как и в Julia 0.6), а в файлах и других неинтерактивных контекстах выводится это прямое предупреждение:
Присваивание
sв мягкой области видимости неоднозначно, так как глобальная переменная с тем же именем существует:sбудет обработана как новая локальная переменная. Разрешите неоднозначность, используяlocal sдля подавления этого предупреждения илиglobal sдля присвоения существующей глобальной переменной.
Это решает обе проблемы, сохраняя преимущества поведения 1.0 для «программирования в масштабе»: глобальные переменные не оказывают странного действия на расстоянии на смысл кода, который может находиться далеко; в REPL отладка копированием-вставкой работает, и начинающие программисты не сталкиваются с проблемами; каждый раз, когда кто-то забывает аннотацию global или случайно скрывает существующую глобальную переменную локальной переменной в мягкой области видимости, что было бы запутанно в любом случае, они получают приятное ясное предупреждение.
Важным свойством этого дизайна является то, что любой код, который выполняется в файле без предупреждения, будет вести себя так же в новом REPL. И наоборот, если вы сохраните сеанс REPL в файл, если он будет вести себя иначе, чем в REPL, вы получите предупреждение.
Блоки let
В отличие от присваиваний локальным переменным, операторы let каждый раз при выполнении выделяют новые связывания переменных. Присвоение изменяет существующее местоположение значения, а let создаёт новые местоположения. Это различие обычно не имеет значения и обнаруживается только в случае переменных, которые выживают за пределами своей области видимости через замыкания. Синтаксис let принимает последовательность присваиваний и имён переменных, разделённых запятыми:
julia> x, y, z = -1, -1, -1;
julia> let x = 1, z
println("x: $x, y: $y") # x is local variable, y the global
println("z: $z") # errors as z has not been assigned yet but is local
end
x: 1, y: -1
ERROR: UndefVarError: z not defined
Присвоения вычисляются в порядке, причём каждое правое выражение вычисляется в области видимости перед тем, как новая переменная в левой части выражения будет введена. Поэтому имеет смысл написать что-то вроде let x = x, поскольку две переменные x различаются и имеют отдельные области хранения. Вот пример, где необходимо поведение let.
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
Fs[i] = ()->i
global i += 1
end
julia> Fs[1]()
3
julia> Fs[2]()
3
Здесь мы создаём и сохраняем две замыкания, которые возвращают переменную i. Однако, это всегда одна и та же переменная i, поэтому оба замыкания ведут себя одинаково. Мы можем использовать let для создания новой привязки для i:
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
let i = i
Fs[i] = ()->i
end
global i += 1
end
julia> Fs[1]()
1
julia> Fs[2]()
2
Поскольку конструкция begin не вводит новую область видимости, может быть полезно использовать let с нулевыми аргументами, чтобы просто ввести новый блок области видимости без создания новых привязок:
julia> let
local x = 1
let
local x = 2
end
x
end
1
Поскольку let вводит новый блок области видимости, внутренняя локальная переменная x — это другая переменная, чем внешняя локальная переменная x.
Циклы и генераторы
В циклах и генераторах новые переменные, введённые в области видимости тела цикла, выделяются заново для каждой итерации цикла, как если бы тело цикла было окружено блоком let, как показано в этом примере:
julia> Fs = Vector{Any}(undef, 2);
julia> for j = 1:2
Fs[j] = ()->j
end
julia> Fs[1]()
1
julia> Fs[2]()
2
Переменная цикла или генератора for всегда является новой переменной:
julia> function f()
i = 0
for i = 1:3
# empty
end
return i
end;
julia> f()
0
Однако иногда бывает полезно повторно использовать существующую локальную переменную в качестве переменной цикла. Это можно удобно сделать, добавив ключевое слово outer:
julia> function f()
i = 0
for outer i = 1:3
# empty
end
return i
end;
julia> f()
3
Константы
Распространённое использование переменных — присвоение имён конкретным неизменяемым значениям. Такие переменные присваиваются только один раз. Это намерение можно передать компилятору, используя ключевое слово const:
julia> const e = 2.71828182845904523536; julia> const pi = 3.14159265358979323846;
Несколько переменных можно объявить в одном операторе const:
julia> const a, b = 1, 2 (1, 2)
Объявление const следует использовать только в глобальной области видимости для глобальных переменных. Компилятору сложно оптимизировать код, включающий глобальные переменные, поскольку их значения (или даже их типы) могут меняться практически в любое время. Если глобальная переменная не будет изменяться, добавление объявления const решает эту проблему производительности.
Локальные константы существенно отличаются. Компилятор автоматически определяет, когда локальная переменная является константой, поэтому объявления локальных констант не нужны, и, на самом деле, в настоящее время не поддерживаются.
Специальные присваивания на верхнем уровне, такие как те, которые выполняются с помощью ключевых слов function и struct , по умолчанию являются константами.
Обратите внимание, что const влияет только на привязку переменной; переменная может быть привязана к изменяемому объекту (например, массиву), и этот объект всё ещё может быть изменён. Кроме того, при попытке присвоить значение переменной, объявленной как константа, возможны следующие варианты:
- если новое значение имеет тип, отличный от типа константы, то выбрасывается ошибка:
julia> const x = 1.0 1.0 julia> x = 1 ERROR: invalid redefinition of constant x
- если новое значение имеет тот же тип, что и константа, то выводится предупреждение:
julia> const y = 1.0 1.0 julia> y = 2.0 WARNING: redefinition of constant y. This may fail, cause incorrect answers, or produce other errors. 2.0
- если присвоение не приведёт к изменению значения переменной, то сообщение не выводится:
julia> const z = 100 100 julia> z = 100 100
Последшее правило применяется к неизменяемым объектам, даже если привязка переменной изменится, например:
julia> const s1 = "1"
"1"
julia> s2 = "1"
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x00000000132c9638
Ptr{UInt8} @0x0000000013dd3d18
julia> s1 = s2
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x0000000013dd3d18
Ptr{UInt8} @0x0000000013dd3d18
Однако для изменяемых объектов предупреждение выводится, как ожидалось:
julia> const a = [1]
1-element Array{Int64,1}:
1
julia> a = [1]
WARNING: redefinition of constant a. This may fail, cause incorrect answers, or produce other errors.
1-element Array{Int64,1}:
1
Обратите внимание, что, хотя иногда это возможно, изменение значения переменной const категорически не рекомендуется и предназначено только для удобства при интерактивном использовании. Изменение констант может привести к различным проблемам или неожиданному поведению. Например, если метод ссылается на константу и уже скомпилирован до изменения константы, то он может продолжать использовать старое значение:
julia> const x = 1 1 julia> f() = x f (generic function with 1 method) julia> f() 1 julia> x = 2 WARNING: redefinition of constant x. This may fail, cause incorrect answers, or produce other errors. 2 julia> f() 1
© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.5.3/manual/variables-and-scoping/