دامنه (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 در آن مکان در حال حاضر به چه چیزی اشاره میکند طبق قوانین زیر عمل میکند:

  1. متغیر محلی موجود: اگر x در حال حاضر یک متغیر محلی است، مقدار مورد نظر درون x قرار میگیرد.
  2. دامنه سخت: اگر x در حال حاضر یک متغیر محلی نیست و مقدار دهی داخل یکی از دامنه های سخت (یعنی داخل یک let block, function or macro body, comprehension, or generator) اتفاق بیفتد، یک متغیر محلی جدید به نام x در دامنه مقدار دهی ساخته میشود.
  3. دامنه نرم: اگر x در حال حاضر یک متغیر محلی نیست و تمام دامنه هایی که شامل مقداردهی هستند دامنه نرم هستند (یعنی loops, try/catch blocks, or struct 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 در خط دوم حلقه محلی است، ولی از آنجایی که هر دو خط در یک بلوک دامنه هستند و هر متغیر تنها میتواند در یک دامنه، یک معنی داشته باشد، این امر امکان پذیر نیست.

درباره دامنه نرم

هم اکنون، ما همه ی قوانین دامنه های محلی را پوشش دادیم. اما قبل از اتمام این قسمت، احتمالا باید کمی درباره اینکه چطور به دامنه نرم مبهم، به صورتی متفاوت در محیط های تعاملی و غیرتعاملی رسیدگی میشود. دو سوال بدیهی وجود دارد که ممکن است یک نفر بپرسد:

  1. چرا در همه جا مانند REPL کار نمیکند؟
  2. چرا همه جا مانند وقتی که در فایل است کار نمیکند؟ و شاید هشدارها را رد میکند؟

در 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 در حلقه، کد را بسیار ناخوانا میکند و اصلا قابل تحمل نیست. اما در حقیقت، دو مشکل اصلی در این رابطه وجود دارد:

  1. دیگر نمیتوان به سادگی قطعه کد داخل یک تابع را در REPL کپی و پیست کرد. برای دیباگ کردن آن، باید متغیر global را اضافه کنید و سپس حذف کنید تا به حالت قبل برگردید.
  2. افراد مبتدی، این کد را بدون نوشتن global مینویسند و اصلا نمیدانند چرا کدشان کار نمیکند. خطایی که آنها دریافت میکنند، این است که متغیر s تعریف نشده، که به نظر کسی را توجیه نمیکند که کجای کار را اشتباه کرده.

از Julia 1.5 به بعد، این قطعه کد بدون کلمه کلیدی global کار میکند اما به صورت تعاملی. مثلا در REPL و یا Jupyter notebooks (دقیقا مانند Julia 0.6). اما در فایل ها و سایر روش های غیر تعاملی، پیام اخطار صریح زیر را نمایش میدهد:

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.

این کار هر دو مشکل را در حالی که مزایای نسخه 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