دامنه (scope) متغیر ها¶
دامنه یک متغیر، ناحیه ای از کد است که متغیر در آن پیدا (قابل دسترسی) است. دامنه بندی کردن متغیر ها کمک میکند تا از تعارض در نامگذاری متغیر ها جلوگیری شود. این ایده به سادگی قابل درک است: دو تابع میتوانند هردو آرگومانی به نام x
داشته باشند بدون اینکه این دو x
به یک چیز اشاره کنند. به طور مشابه، موقعیت های بسیاری هستند که بلوک های مختلف کد میتوانند از یک نام متغیر استفاده کنند بدون اینکه به یک چیز اشاره کنند. قوانین در زمینه اینکه چه زمانی یک نام متغیر به یک چیز اشاره میکنند یا نه، قوانین دامنه نامیده می شوند؛ این بخش آنها را به صورت دقیق توضیح میدهد.
برخی از ساختار ها در زبان، بلوک های دامنه را معرفی میکنند، که بخش هایی از کد هستند که میتوانند دامنه مجموعه هایی از متغیر ها باشند. اسکوپ یک متغیر نمیتواند چند خط دلخواه از کد باشد؛ بلکه حتما باید با یکی از این بلوک ها هم راستا شود. در جولیا دو نوع کلی دامنه وجود دارد، دامنه سراسری و دامنه محلی. مورد دوم میتواند تو در تو نیز باشد. همچنین در جولیا تفاوتی بین ساختار هایی که از "دامنه سخت" استفاده میکنند و آنهایی که از "دامنه نرم" استفاده میکنند وجود دارد، که روی اینکه shadow کردن یک متغیر سراسری با همان نام مجاز است یا نه، تاثیر گذار است.
ساختار های دامنه دار¶
ساختار هایی که بلاک های دامنه را معرفی میکنند:
ساختار | نوع دامنه | مجاز برای استفاده درون |
---|---|---|
module , baremodule |
سراسری | سراسری |
struct |
محلی (نرم) | سراسری |
for , while , try |
محلی (نرم) | سراسری، محلی |
macro |
محلی (سخت) | سراسری |
توابع، بلوک های do ، بلوک های let ، عبارات comprehension، سازنده ها |
محلی (سخت) | سراسری، محلی |
موارد قابل ذکری که در این جدول نیامده اند عبارتند از [بلوک های شروع](@ref man-compound-expressions) و [بلوک های شرطی](@ref man-conditional-evaluation) که دامنه جدیدی معرفی نمیکنند. این سه نوع دامنه قوانین بعضا متفاوتی دارند که در زیر توضیح داده خواهد شد.
جولیا از lexical scoping (دامنه واژگانی)،
استفاده میکند، یعنی دامنه یک تابع از دامنه تابعی که آن را صدا زده تبعیت نمیکند، بلکه از دامنه ای تبعیت میکند که تابع در آن تعریف شده است. برای مثال، در کد زیر 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](@ref modules) یا به وسیله دسترسی مشروط با استفاده از نقطه گذاری استفاده کنند، یعنی هر ماژول هم یک فضای نام است و هم یک ساختمان داده سطح اول که نام ها را به مقدار ها ربط میدهد. توجه کنید که در حالی که binding های یک متغیر میتواند توسط کد خارجی خوانده شود، مقدار انها تنها در ماژولی که در آن تعریف شده اند قابل تغییر است. به عنوان یک راه فرار، شما همیشه میتوانید به یک کد اعتبار بدهید تا داخل آن ماژول مقداری را تغییر دهد؛ این شرایط به طور خاص تضمین میکند که binding های ماژول ها نمیتوانند توسط کدی که 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
قرار دارد.
دامنه محلی¶
یک دامنه محلی جدید توسط اکثر بلوک های کد تعریف میشود ([جدول](@ref man-scope-table) بالا را برای یک لیست کامل نگاه کنید). بعضی از زبان های برنامه نویسی نیاز به تعریف صریح متغیر قبل از استفاده از آن دارد. تعریف صریح در جولیا هم کار میکند: در هر دامنه محلی، نوشتن local x
یک متغیر محلی جدید در آن دامنه تعریف میکند، بدون توجه به اینکه آیا در حال حاضر متغیری به نام x
در یک دامنه خارجی تعریف شده است یا نه. اگرچه تعریف هر متغیر محلی به این صورت طولانی و خسته کننده است، پس جولیا، مثل خیلی از زبان های دیگر، مقدار دهی به یک متغیر جدید در یک دامنه محلی را معادل تعریف صریح آن متغیر به صورت محلی میداند. این اتفاق عموما شهودی است، اما مانند خیلی چیز های دیگر که به این شکل هستند، جزئیات ظریف تر از آن هستند که یک نفر ممکن است به صورت ساده لوحانه تصور کند.
هنگامی که <x = <value
در یک دامنه محلی نمایان میشود، جولیا برای اینکه بفهمد این عبارت چه معنایی دارد با توجه به مکان عبارت و اینکه x
در آن مکان در حال حاضر به چه چیزی اشاره میکند طبق قوانین زیر عمل میکند:
- متغیر محلی موجود: اگر
x
در حال حاضر یک متغیر محلی است، مقدار مورد نظر درونx
قرار میگیرد. - دامنه سخت: اگر
x
در حال حاضر یک متغیر محلی نیست و مقدار دهی داخل یکی از دامنه های سخت (یعنی داخل یک let block, function or macro body, comprehension, or generator) اتفاق بیفتد، یک متغیر محلی جدید به نامx
در دامنه مقدار دهی ساخته میشود. - دامنه نرم: اگر
x
در حال حاضر یک متغیر محلی نیست و تمام دامنه هایی که شامل مقداردهی هستند دامنه نرم هستند (یعنی loops,try
/catch
blocks, orstruct
blocks)، رفتار زبان بستگی به این دارد که آیاx
به عنوان یک متغیر سراسری تعریف شده است یا نه:- اگر متغیر سراسری
x
تعریف نشده است، یک متغیر محلی جدید به نامx
در دامنه مقداردهی ساخته میشود. - اگر متغیر سراسری
x
تعریف شده است، مقداردهی، مبهم در نظر گرفته میشود:- در محیط های غیر تعاملی (فایل، eval)، یک اخطار مبهم بودن چاپ شده و یک متغیر محلی جدید تعریف میشود.
- در محیط های تعاملی (REPL، notebook)، متغیر سراسری
x
مقداردهی میشود.
- اگر متغیر سراسری
به این نکته توجه کنید که در محیط های غیر تعاملی، رفتار در صورت وجود یا عدم وجود متغیر سراسری با همان اسم یکسان است، تنها زمانی که یک متغیر محلی ضمنی (یعنی تعریف نشده با local x
) shadows یک متغیر سراسری، یک اخطار چاپ میشود.
در محیط های تعاملی، جهت سادگی، قوانین از ابتکارات پیچیده تری پیروی میکنند که به طور عمیق تر، در مثال هایی که در ادامه آمده اند پوشش داده شده اند.
حالا که قوانین را میدانید، نگاهی به چند مثال بیاندازیم، فرض میکنیم هر مثال در یک نشست 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
وجود دارد یا نه. به عنوان مثال اینجا ما قبل از تعریف و صدا زدن greet
، متغیر سراسری x = 123
را تعریف میکنیم:
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 خروجی میگیریم.
بیایید با نوشتن یک نسخه بلند تر که آن را sum_to_def
مینامیم، به این نکته بپردازیم که بدنه حلقه for
دامنه خود را دارد.
در این نسخه قبل از بروزرسانی s
مقدار s + i
را در متغیری به نام t
ذخیره میکنیم:
julia> function sum_to_def(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_def (generic function with 1 method)
julia> sum_to_def(10)
(55, false)
این نسخه هم مثل قبل s
را برمیگرداند ولی بعلاوه، از ماکرو @isdefined
استفاده شده تا نشان دهد آیا یک متغیر محلی به نام t
در بیرونی ترین دامنه محلی تابع تعریف شده است یا نه. همانطور که میبینید، t
بیرون از بدنه حلقه for
تعریف نشده است. این هم به علت قوانین دامنه های سخت است: از آنجایی که مقداردهی به t
داخل یک تابع اتفاق می افتد، که یک دامنه سخت دارد، مقداردهی باعث میشود t
یک متغیر محلی جدید درون دامنه محلی که در آن آمده است باشدف یعنی درون بدنه حلقه. حتی اگر یک متغیر سراسری به نام t
وجود داشت، تغییری ایجاد نمیکرد—قوانین دامنه سخت با هیچ چیزی در دامنه سراسری تغییر نمیکند.
بیایید به چند مورد مبهم تر در زمینه دامنه های نرم بپردازیم. این کار را با استخراج بدنه های توابع greet
و sum_to_def
به محیط هایی با دامنه نرم انجام میدهیم. ابتدا، بیایید بدنه تابع 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_def
را در دامنه سراسری قرار دهیم، برای اینکار آرگومان آن را روی 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
و یا comprehension)، بر حسب اینکه آیا متغیر سراسری به نام 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
یک خطای method error میباشد، پس محتمل است که هدف این است که x
در حلقه for
به صورت محلی باشد.
اما متغیر های زمان اجرا و اینکه چه متد هایی وجود دارند نمیتوانند ملاک تعیین دامنه متغیر ها باشند.
با رفتار Julia ≤ 0.6، ممکن است یک نفر حلقه for
را ابتدائا نوشته باشد و به خوبی کار کند. اما بعدا، یک نفر با تعریف یک متغیر گلوبال (که ممکن است بسیار دورتر از این حلقه باشد مثلا در یک فایل دیگر باشد)
معنای کد را به یکباره عوض میکند و یا یک با سر و صدا کد را خراب میکند، و یا حتی بدتر از آن، به صورت نامحسوس جواب اشتباه میدهد.
اینگونه "اتفاقات ترسناک از دور"، چیزهایی هستند که طراحی خوب زبان های برنامه نویسی باید از اتفاق افتادن آن جلوگیری کند.
بنابراین در Julia 1.0، ما قوانین دامنه را به این صورت بیان میکنیم: در هر دامنه محلی، مقداردهی به یک نام که قبلا به عنوان یک متغیر محلی نبوده، یک متغیر محلی جدید میسازد. اینکار ایده دامنه نرم را به طور کامل حذف میکند. اما از طرفی پتانسیل کارهای شبهه برانگیز را از بین میبرد. ما تعداد زیادی باگ را با استفاده از ایده حذف دامنه نرم حل کردیم، بنابراین از ایده حذف دامنه نرم دفاع میکنیم و بسیار باعث خوشحالی بود. اما، نه به صورت کامل. چراکه بعضی عصبانی بودند که از این به بعد باید بنویسند:
s = 0
for i = 1:10
global s += i
end
کلمه کلیدی global
در حلقه، کد را بسیار ناخوانا میکند و اصلا قابل تحمل نیست. اما در حقیقت، دو مشکل اصلی در این رابطه وجود دارد:
- دیگر نمیتوان به سادگی قطعه کد داخل یک تابع را در REPL کپی و پیست کرد. برای دیباگ کردن آن، باید متغیر
global
را اضافه کنید و سپس حذف کنید تا به حالت قبل برگردید. - افراد مبتدی، این کد را بدون نوشتن
global
مینویسند و اصلا نمیدانند چرا کدشان کار نمیکند. خطایی که آنها دریافت میکنند، این است که متغیرs
تعریف نشده، که به نظر کسی را توجیه نمیکند که کجای کار را اشتباه کرده.
از Julia 1.5 به بعد، این قطعه کد بدون کلمه کلیدی global
کار میکند اما به صورت تعاملی. مثلا در REPL و یا Jupyter notebooks (دقیقا مانند Julia 0.6). اما در فایل ها و سایر روش های غیر تعاملی، پیام اخطار صریح زیر را نمایش میدهد:
Assignment tos
in soft scope is ambiguous because a global variable by the same name exists:s
will be treated as a new local. Disambiguate by usinglocal s
to suppress this warning orglobal s
to assign to the existing global variable.
این کار هر دو مشکل را در حالی که مزایای نسخه 1.0 در برنامه نویسی با وسعت را حفظ میکند، حل میکند:
متغیر های سراسری هیچ تاثیر عجیبی روی کدی که ممکن است خیلی دور باشد ندارد؛
دیباگ کردن به روش کپی-پیست در REPL کار میکند و برای مبتدی ها هیچ مشکلی در این زمینه وجود ندارد. هرگاه یک نفر فراموش کند از global
استفاده کند و یا به صورت تصادفی از یک متغیر محلی به جای متغیر موجود سراسری با همان نام در یک دامنه استفاده کند (که در هر صورت گیج کننده خواهد بود)، یک پیام هشدار واضح دریافت میکنند.
یک ویژگی مهم این طراحی این است که هر کدی که در فایل بدون اخطار اجرا شود، دقیقا مانند کدی عمل میکند که در فضای REPL اجرا شود. و به علاوه، اگر یک session از REPL را در فایل ذخیره کنیم، اگر به صورتی متفاوت از REPL رفتار کرد، در اینصورت یک پیام هشدار دریافت خواهید کرد.
بلوک های Let¶
برخلاف مقداردهی به متغیرهای محلی، عبارت های let
هر بار که اجرا میشوند، binding های جدیدی به متغیر اختصاص میدهند.
یک مقداردهی، مکان یک متغیر موجود را ویرایش میکند، اما let
مکان های جدید میسازد.
این تفاوت معمولا مهم نیست، و تنها زمانی قابل تشخیص است که متغیر خارج از دامنه خود قابل دسترسی باشد (اینکار توسط closure ها قابل انجام است).
عبارت 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
در اینجا ما دو closure میسازیم و ذخیره میکنیم که متغیر i
را بر میگردانند.
اما این i
همواره یکسان است. در نتیجه این دو closure مثل هم عمل میکنند.
میتوانیم برای اینکه یک binding جدید برای i
بسازیم، از let
استفاده کنیم:
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
بدون آرگومان مفید باشد، و آن هم برای اینکه بدون ساختن binding جدید یک بلوک دامنه جدید تعریف کنیم:
julia> let
local x = 1
let
local x = 2
end
x
end
1
از آنجا که let
یک بلوک دامنه جدید را معرفی میکند، متغیر x
داخلی که به صورت محلی است، یک متغیر جدا از متغیر محلی x
هست که به صورت خارجی تعریف شده.
حلقه ها و Comprehensions¶
در حلقه ها و [comprehensions](@ref man-comprehensions)، متغیر های جدید تعریف شده در بدنه آنها، با هر بار اجرای حلقه ها دوباره تعریف میشوند، انگار که بدنه حلقه درون یک بلوک 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
و یا یک متغیر تکرار comprehension، یک متغیر جدید است. به عبارتی در مثال زیر، متغیر for
که به صورت for i = 1:3
تعریف شده، متفاوت از متغیر تعریف شده در خط بالاتر است:
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
فقط بر binding متغیر تاثیر میگذارد. متغیر ممکن است مقید شده باشد به یک شیء قابل تغییر (مثل آرایه)، و آن شیء میتواند مقدار بگیرد. به علاوه، هنگامی که سعی شود به یک متغیر ثابت مقداردهی شود، سناریوهای زیر محتمل هستند:
- اگر متغیر جدید، نوع داده متفاوتی از متغیر ثابت اولیه داشته باشد، خطای زیر داده میشود:
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
قانون آخر بر روی اشیاء غیرقابل تغییر اعمال میشود حتی اگر binding متغیر تغییر کند. برای مثال:
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 Vector{Int64}:
1
julia> a = [1]
WARNING: redefinition of constant a. This may fail, cause incorrect answers, or produce other errors.
1-element Vector{Int64}:
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