Julia یک زبان همهمنظوره است که میتوان از آن برای بیشتر کارهای برنامهنویسی استفاده کرد. اما در عمل، کاربردهای اصلی آن بیشتر در مهندسی و علوم است. محاسبات عددی سریع، منعطف و پیشرفته در قلب طراحی آن قرار دارند.
یک عدد صحیح عددی «گرد» است که نقطهی اعشاری ندارد.
در مفهوم مبانی دیدیم که میتوان یک مقدار صحیح را بدون تعیین نوع به یک متغیر اختصاص داد.
برای خوانایی بیشتر، میتوان از زیرخط بهعنوان جداکنندهی رقمها استفاده کرد. کامپایلر آنها را نادیده میگیرد.
julia> x = 3
3
julia> typeof(x)
Int64
julia> large_number = 1_234_567_890
1234567890
در داخل، کامپایلر از هر نوع عدد صحیح علامتداری که برای CPU شما مناسبتر باشد استفاده میکند. روی رایانههای شخصی مدرن، این نوع معمولاً Int64 است که برای بیشتر کارها کاملاً کافی است.
نوعها در مفهوم دیگری در آینده با جزئیات بیشتری بررسی میشوند. فعلاً امیدواریم نوعهای عددی بهقدری شهودی باشند که این متن قابل فهم باشد.
همانطور که در مفهوم دیگری در ادامه خواهیم دید، Julia بهصورت بومی از آرایههای بسیار بزرگ و چندبعدی پشتیبانی میکند. این آرایهها هم مقدار حافظهی در دسترس و هم پهنای باند انتقال داده را به چالش میکشند.
برای دادن کنترل دقیقتر به برنامهنویس، تعیین نوع عدد صحیح علامتدار یا بیعلامت دلخواه هم ممکن است و هم کاملاً رایج. بسیاری از یادگیرندگان Julia که پیشتر از Python استفاده کردهاند، ممکن است بخواهند این را بهجای Python پایه با NumPy مقایسه کنند.
julia> y = Int8(42)
42
julia> typeof(y)
Int8
julia> z = UInt32(1024)
0x00000400
توجه کنید که Julia بهطور پیشفرض اعداد صحیح بیعلامت را در قالب مبنای شانزده نمایش میدهد، چون نوعهای بیعلامت اغلب برای عملیات بیتی سطح پایین استفاده میشوند.
اعداد صحیح را میتوان بهصورت دودویی، مبنای هشت یا مبنای شانزده هم وارد کرد، با پیشوندهای 0b، 0o و 0x بهترتیب.
julia> a = 0x10
0x10
julia> Int(a)
16
julia> b = 0b1010 # displays as hexadecimal
0x0a
BigInt
هر نوع عدد صحیح مقدار بیشینه و کمینهای دارد که میتواند ذخیره کند:
julia> typemax(Int8)
127
julia> typemin(Int8)
-128
خروج از این بازهی معتبر باعث سرریز عدد صحیح میشود، با نتیجههایی که بسیار عجیب به نظر میرسند.
julia> c = Int8(126)
126
julia> c * c
4
احتمالاً همه قبول داریم که 126 * 126 نباید برابر ۴ باشد!
توضیح دقیق نیاز به دانشی دربارهی نحوهی ذخیرهسازی اعداد صحیح در ثباتهای CPU دارد که خارج از حوزهی این مفهوم است.
میتوان این مشکل را با استفاده از نوعی «پهنتر»، تا Int128 یا UInt128، به کمترین حد رساند (معنای «پهنتر» در ادامه بحث میشود). اما همهی آنها پیش از برخورد با مشکل سرریز، محدودیتهای سختی دارند.
یک راهحل استفاده از نوع BigInt است که تنها به حافظهی رایانهی شما محدود میشود. این نوع، پیادهسازی Julia از حساب با دقت دلخواه است.
julia> 2 ^ 70
0
julia> big = BigInt(2) ^ 70
1180591620717411303424
julia> UInt128(big)
0x00000000000000400000000000000000
چون ۲ به توان ۷۰ در یک Int64 جا نمیشود، محاسبهی اول در مثال بالا شکست میخورد: هرچند توجه کنید که پیام خطایی وجود ندارد. برنامهنویس، مراقب باشید!
تبدیل نوع عدد ۲ به BigInt پاسخ درست را میدهد و نمایش آن در مبنای شانزده روشنتر میکند که این عدد توانی از ۲ است.
جای تعجب نیست که اعداد ممیز شناور بهصورت اختیاری نقطهی اعشاری دارند و بخش کسری پس از آن نقطه میآید.
julia> f = 3.45
3.45
julia> typeof(f)
Float64
البته نمایش علمی پشتیبانی میشود.
julia> avogadro = 6.02e23
6.02e23
مانند اعداد صحیح، نوع پیشفرض برای بیشتر کارها مناسب است، اما نوعهای علامتدار دیگری هم وجود دارند. هیچ نوع ممیز شناور بیعلامت وجود ندارد.
بهعنوان یک میانبر، میتوان مقادیر Float32 را با استفاده از پسوند f0 ساخت.
julia> f32 = 4.56f0
4.56f0
julia> typeof(f32)
Float32
سایر اندازههای شناور به یک تبدیل نوع صریح نیاز دارند، مانند اعداد صحیح.
مقدارهای بیشینه و کمینه ممکن است غافلگیرکننده باشند:
julia> typemax(Float64)
Inf
julia> typemin(Float64)
-Inf
بینهایت یک مقدار معتبر است!
اما بازهی مفید اعداد ممیز شناور محدود است: هر مقدار بسیار بزرگ صرفاً مقدار Inf میگیرد و هر مقدار بسیار کوچک به 0.0 گرد میشود.
میتوانیم با جفت توابع دیگری این محدودیتها را ببینیم: تقریباً ± 10 ^ 308.
julia> floatmax(Float64)
1.7976931348623157e308
julia> floatmin(Float64)
2.2250738585072014e-308
همچنین دقت محدود است و تعداد زیادی، اما متناهی، از رقمهای بامعنا نمایش داده میشود (تقریباً ۱۵ برای Float64).
همانطور که در مفهوم مبانی بحث شد، عملگرهای حساب عمدتاً مانند حساب استانداردی که به کودکان آموزش داده میشود کار میکنند. توجه کنید که توانرسانی از ^ استفاده میکند، نه ** (هر دو در زبانهای دیگر رایجاند).
2 + 3 # 5 (addition)
2 - 3 # -1 (subtraction)
2 * 3 # 6 (multiplication)
8 / 2 # 4.0 (division)
8 % 3 # 2 (remainder)
2 ^ 3 # 8 (exponentiation)
اما چند نکتهی مخصوص Julia ارزش بحث دارند.
julia> x = 4.2
4.2
julia> 2 * x
8.4
julia> 2x
8.4
julia> 2.4x
10.08
این ممکن است غافلگیرکننده باشد.
همیشه میتوان از * بهعنوان عملگر میانوندی استفاده کرد، مانند بیشتر زبانهای رایانهای دیگر.
اما Julia را افرادی طراحی کردهاند که باور دارند کد باید تا حد امکان شبیه معادلات ریاضی به نظر برسد.
چون اسم متغیرها باید با یک حرف شروع شود، آوردن یک عدد (صحیح یا ممیز شناور) پیش از اسم بهعنوان ضرب ضمنی در نظر گرفته میشود.
برای مثال، اگر مساحت سطح یک کره را بخواهیم، بهجای 4 * pi * r * r میتوانیم این کار را انجام دهیم:
julia> surface(r) = 4π * r^2
surface (generic function with 1 method)
julia> surface(3)
113.09733552923255
هرچند π یک ثابت از پیش تعریفشده است، یک حرف (یونانی) هم هست. بنابراین تجزیهگر همچنان به یک * صریح نیاز دارد تا π را از r جدا کند.
استفاده از / بهعنوان عملگر میانوندی همیشه نتیجهای ممیز شناور میدهد، حتی برای ورودیهای صحیح.
برای تقسیم صحیح، گزینههای بیشتری هست:
julia> 10 / 3
3.3333333333333335
julia> div(10, 3)
3
julia> 10 ÷ 3
3
julia> 10 // 3
10//3
تابع div() برای تقسیم صحیح است و نتیجه به سمت صفر بریده میشود: برای اعداد مثبت به پایین و برای اعداد منفی به بالا.
بهعنوان مترادف، میتوانیم از عملگر میانوندی ÷ استفاده کنیم، باز هم با هدف شبیهتر کردن آن به ریاضیات. اگر از ویرایشگری استفاده میکنید که Julia را میشناسد، این را بهصورت \div وارد کنید و سپس کلید <Tab> را بزنید.
عملگر // در ادامهی برنامهی درسی، به مفهوم جداگانهی خودش نیاز خواهد داشت.
فعلاً میتوانیم فقط بگوییم که نتیجهی // یک عدد «گویا» است؛ نام رسمی چیزی که بیشتر مردم آن را کسر مینامند.
عاملهای مشترک از صورت و مخرج حذف میشوند تا نسبتی از دو عدد صحیح در آنچه «سادهترین شکل» نامیده میشود بهدست آید.
julia> rationalnum = 22 // 6
11//3
julia> typeof(rationalnum)
Rational{Int64}
عددهای گویا را داریم. پس عددهای «گنگ» چطور؟
julia> π
π = 3.1415926535897...
julia> typeof(π)
Irrational{:π}
عدد گنگ عددی است که نمیتوان آن را به نسبتی از اعداد صحیح کاهش داد. مثالهای رایج شامل π، e (عدد اویلر) و بسیاری از ریشهها مانند √2 (جذر ۲) است.
Julia تلاش میکند ریاضیات را درست و اصولی انجام دهد.
این کار اغلب میتواند بهطور خودکار انجام شود:
julia> x = 2 + 3.5
5.5
julia> typeof(x)
Float64
یک Int64 را به یک Float64 اضافه کردیم و نتیجهای از نوع Float64 گرفتیم.
در واقع، عدد صحیح پیش از انجام جمع، بیصدا به Float64 تبدیل شد.
Julia مفهومی به نام «پهنا» برای نوعهای عددی دارد.
Int64 از Int16 پهنتر است.اگر در یک عبارت از ترکیبی از نوعها استفاده شود، هر کدام در صورت نیاز به پهنترین نوعِ بهکاررفته «ارتقا» داده میشود.
برای اجباری کردن تبدیل، میتوانیم یک عدد صحیح را به نوعی خاص تبدیل کنیم، مانند Float64(5).
در غیر این صورت، فقط از float(5) استفاده کنید و بگذارید کامپایلر نوع مناسبی را انتخاب کند.
تبدیلهای ممیز شناور به صحیح ناگزیر پیچیدهترند. میخواهید با هر چیزی که بعد از نقطهی اعشاری میآید چه کار کنید؟
round() به نزدیکترین عدد کامل گرد میکند و در حالتهای تساوی مانند ۴٫۵ به نزدیکترین عدد کامل زوج گرد میشود.floor() به پایین، ceil() به بالا و trunc() به سمت صفر گرد میکند.Int32()، با یک InexactError شکست میخورد.اما این توابع بهطور پیشفرض نوع صحیحی را که شاید میخواستید برنمیگردانند. میتوان نوع خروجی مورد نظر را مشخص کرد.
julia> round(4.5)
4.0
julia> round(Int64, 4.5)
4
julia> round(Int, 4.5) # => default integer type
4
julia> ceil(Int16, 4.3)
5
گرد کردن به تعداد مشخصی از رقمها پس از نقطهی اعشاری هم با کلیدواژهی digits ممکن است.
julia> round(π, digits=10)
3.1415926536
برای جزئیات بیشتر به راهنما مراجعه کنید.
قطعاً این فقط یک خطا پرتاب میکند؟ در واقع، ماجرا به این سادگی نیست.
تقسیم صحیح با ÷ یا // همانطور که انتظار دارید به خطا منجر میشود.
تقسیم ممیز شناور با / رویکردی را در پیش میگیرد که ممکن است رویکردی مهندسی در نظر گرفته شود، نه رویکردی استاندارد از علوم رایانه:
julia> 2 / 0
Inf
julia> 0 / 0
NaN
همانطور که در بخش پیشین بحث شد، بینهایت در Julia یک عدد ممیز شناور معتبر است که با Inf نمایش داده میشود.
وقتی صورت هم صفر باشد، نتیجه از نظر ریاضی تعریفنشده است. Julia در این حالت آن را «عدد نیست» در نظر میگیرد که با NaN نمایش داده میشود.
اگر این عجیب به نظر میرسد، آن را در بافت کار با آرایههای بزرگ دادههای دنیای واقعی (و بنابراین اغلب نسبتاً آشفته) در نظر بگیرید. برای پیشرفت، بهترین کار این است که مقادیر مشکلدار را علامتگذاری کنید و جلو بروید.
بررسی دستی بیپایان مقادیر، برنامهنویسیاش خستهکننده میشد و قطعاً به کارایی زمان اجرا آسیب میزد.
توقف با یک پیام خطا در هر لغزش کوچک، برنامهی شما را نزد کاربران بسیار نامحبوب میکرد!
همانطور که در مفهوم Conditionals توضیح داده شد، آزمون تساوی معمولاً با عملگر == انجام میشود.
این کار برای اعداد صحیح، نویسهها، رشتهها و غیره خوب عمل میکند. اما مقادیر ممیز شناور دقت محدودی دارند و روشهای مختلف محاسبهی یک نتیجهی یکسان میتوانند به مقادیر کمی متفاوت منجر شوند. برای Float64، این معمولاً حدود رقم بامعنای پانزدهم است: تفاوتی کوچک، اما نه «تساوی».
بهطور سنتی، توصیه به برنامهنویسان این است که هرگز از == با مقادیر ممیز شناور استفاده نکنند: نتیجهها غیرقابل پیشبینیاند.
جایگزین پرکاربردی این است که قدر مطلق تفاضل را با یک تلورانس مجاز (اغلب اپسیلون یا ϵ نامیده میشود) بسنجیم. بنابراین بهجای a == b از abs(a - b) < epsilon استفاده کنید.
Julia جایگزین تمیزتری با تابع isapprox() ارائه میدهد.
برای یک تلورانس مطلق، نحوهی نگارش isapprox(a, b, atol=epsilon است. در این حالت کلیدواژهی atol الزامی است.
تلورانس نسبی اغلب مفیدتر است. این حالت پیشفرض است، بنابراین isapprox(a, b) تلاش میکند مقدار معقولی برای rtol انتخاب کند (قواعدش نسبتاً پیچیده است).
میتوان تلورانس نسبی را نیز، بهصورت کسری از مقادیری که مقایسه میشوند، مشخص کرد. بنابراین isapprox(a, b, rtol=0.01 بررسی میکند که مقادیر در محدودهی ۱٪ یکدیگر باشند.
به شیوهی معمول Julia، عملگر ریاضیای وجود دارد که مترادف حالت پیشفرض است: a ≈ b (که عملگر آن با وارد کردن \approx و سپس <tab> وارد میشود).
علاوه بر اعداد گویا، بخشهای بعدی برنامهی درسی به این موارد خواهند پرداخت:
2.3 + 4.5im.