۲ - ۸توابع عددی در هسکل

تو این بخش به بررسی چندتا از توابع و عملگرهای پایه‌ایِ حساب می‌پردازیم، که از این قرارند:

عملگراسمکاربرد
‏‎+‎‏به علاوهجمع
‏‎-‎‏منهاتفریق
‏‎*‎‏ستارهضرب
‏‎/‎‏خط موربتقسیم کسری
‏‎div‎‏تقسیمتقسیم صحیح، گِرد به پایین
‏‎mod‎‏پیمانهباقیمانده از تقسیم پیمانه‌ای
‏‎quot‎‏خارج‌قسمتتقسیم صحیح، گِرد به سمت صفر
‏‎rem‎‏باقیماندهباقیمانده از تقسیم

احتمالاً براتون واضحه، اما منظور از تقسیمِ صحیح، تقسیمِ اعدادِ integer یا صحیح ِه. اون توابعْ اعدادِ کسری نمی‌گیرن و فقط اعداد integer می‌گیرن. به همین خاطر هم جوابشون گِرد میشه.

در زیر از هر کدوم یه مثال تو REPL زدیم:

Prelude> 1 + 1
2
Prelude> 1 - 1
0
Prelude> 1 * 1
1 
Prelude> 1 / 1
1.0
Prelude> div 1 1
1
Prelude> mod 1 1
0
Prelude> quot 1 1
1
Prelude> rem 1 1
0

به خاطرِ طریقه‌ی گرد کردنِ ‏‎div‎‏ و ‏‎quot‎‏، کاربردِ ‏‎div‎‏ برای تقسیمِ صحیح بیشتره:

-- به پایین گرد میشه
Prelude> div 20 (-6)
-4

-- به سمت صفر گرد میشه
Prelude> quot 20 (-6)
-3

کاربرد ‏‎rem‎‏ و ‏‎mod‎‏ هم یه کم با هم فرق دارن؛ تو این فصل ‏‎mod‎‏ رو با جزئیاتِ بیشتری بررسی می‌کنیم. تقسیم کسری (‏‎/‎‏) رو تو یه فصل جلوتر توضیح میدیم، چون قبلش باید کمی با تایپ‌ها و تایپکلاس‌ها آشنایی پیدا کنیم.

قوانین خارج‌قسمت و باقیمانده

در برنامه‌نویسی، اکثراً توابعی که برای تقسیم و باقیمانده وجود دارن بیشتر از ریاضیه، و برای درک بهتر، آشنایی با قوانین مرتبط با ‏‎quot‎‏ و ‏‎rem‎‏، و ‏‎div‎‏ و ‏‎mod‎‏ خیلی کمک می‌کنه.* اینجا یه نگاه بهشون میندازیم.

(quot x y)*y + (rem x y) == x

(div x y)*y + (mod x y) == x

مراحل اثبات این دو تا رو اینجا نمی‌نویسیم، ولی می‌تونیم با مثال تا حدی مفهوم‌شون رو نشون بدیم:

(quot x y)*y + (rem x y) == x

(-۴) برابر y برابر ۱۰ و x با فرض

(quot 10 (-4))*(-4) + (rem 10 (-4))

quot 10 (-4) == (-2) و rem 10 (-4) == 2

(-2)*(-4) + (2) == 10

10 == x

به جوابی که می‌خواستیم رسیدیم.

حالا برای ‏‎div‎‏ و ‏‎mod‎‏:

(div x y)*y + (mod x y) == x

(-۴) برابر y برابر ۱۰ و x با فرض

(div 10 (-4))*(-4) + (mod 10 (-4))

div 10 (-4) == (-3) و mod 10 (-4) == -2

(-3)*(-4) + (-2) == 10

10 == x

به نتیجه میرسیم که در دنیای تقسیم اعداد صحیح، همه چیز خوب و خوشه.

استفاده از ‏‎mod‎‏

ما اینجا به توضیح کامل حساب پیمانه‌ای نمی‌پردازیم، ولی می‌خوایم یه درکِ کلی از ‏‎mod‎‏ به کسانی که باهاش آشنایی ندارن بدیم. و اینکه دقیقاً تو هسکل چطوری کار می‌کنه.

بالاتر توی جدول گفتیم که ‏‎mod‎‏ باقیمانده‌ی تقسیم ماژولار رو میده. بدون آشنایی با تقسیم ماژولار، تفاوتِ پرفایده‌ی بین ‏‎mod‎‏ و ‏‎rem‎‏ خیلی واضح نمیشه.

حساب پیمانه‌ای یک سیستمی از محاسبات اعداد صحیحه که اعداد بعد از رسیدن به یه مقدار معلوم (به اسم مدول)، "دور میزنن" به مقدار اولیه. مثال رایج برای توضیح این سیستم، ساعته.

وقتی زمان رو بر حسب یه ساعت ۱۲-ساعته می‌شماریم، بعد از ساعت ۱۲ باید شمارش‌مون رو از اول شروع کنیم. برای مثال اگه الان ساعت ۸:۰۰ باشه، و بخوایم بدونیم ۸ ساعت دیگه ساعت چند میشه، نمیگیم ۸ + ۸، پس ساعت میشه ۱۶*.

به جای اون کار، هر ۱۲ ساعت شمارش رو از اول شروع می‌کنیم. پس اگه بخوایم ۸ ساعت به ۸:۰۰ اضافه کنیم، اول ۴ ساعت اضافه می‌کنیم تا به ۱۲ برسیم، و بعد از اول شروع می‌کنیم، انگار ۱۲ همون صِفره، و ۴ ساعت باقیمانده از ۸ ساعت‌مون رو اضافه می‌کنیم تا به ساعت ۴:۰۰ برسیم. بنابراین، ۸ ساعت بعد از ۸:۰۰، ساعت میشه ۴:۰۰.

*

البته در ساعت‌های ۲۴-ساعته، ساعت ۱۶:۰۰ درسته و معنی میده؛ ولی اگه ۸ ساعت بعد از ۸:۰۰ بعدازظهر رو بخوایم، جواب ۱۶:۰۰ صبح نمیشه. مدول در ساعت‌های ۲۴-ساعته با ساعت‌های ۱۲-ساعته متفاوته.

به چنین چیزی میگیم پیمانه ِ ۱۲. در یک ساعتِ ۱۲-ساعته، ۱۲ با خودش و صفر برابره، و به نوعی ساعت ۱۲:۰۰ ساعت ۰:۰۰ هم هست. منظور از پیمانه ِ ۱۲ اینه که ۱۲، هم ۱۲ ِه، و هم صفر.

برای چنین کاربردی، بیشترِ مواقع ‏‎mod‎‏ و ‏‎rem‎‏ یه جواب میدن:

Prelude> mod 15 12
3
Prelude> rem 15 12
3

Prelude> mod 21 12
9
Prelude> rem 21 12
9

Prelude> mod 3 12
3
Prelude> rem 3 12
3

دو تا آخری شاید عجیب باشن... ‏‎rem‎‏ و ‏‎mod‎‏ فقط با تقسیم صحیح کار می‌کنن، پس جوابِ تقسیم با یه عدد بزرگتر، مساوی میشه با صفر، به اضافه‌ی باقیمانده‌ای که برابر با همون عددِ کوچکتر (صورت کسر یا مقسوم) هست. هر وقت بخوایم از تقسیم با یه عددِ بزرگتر جواب کسری بگیریم، از (‏‎/‎‏) استفاده می‌کنیم و دیگه باقیمانده هم نداریم.

فرض کنید یه تابع بخوایم که به ما بگه چند روز بعد یا قبل از امروز، چه روزی از هفته میشه. اول باید هر روزِ هفته رو با یه عدد معادل کنیم، پس شنبه رو با صفر نشون میدیم*. اگه امروز یکشنبه باشه، و بخوایم بدونیم ۲۳ روز دیگه چند شنبه‌ست، چنین کاری جواب میده:

Prelude> mod (6 + 5) 7
4

عدد ۱ برای یکشنبه‌ست (روز مبدأ)، ۲۳ هم تعداد روزیه که می‌خوایم اضافه کنیم. وقتی با ‏‎mod‎‏ ِعدد ۷ حساب کنیم، عددی رو برمی‌گردونه که نشون دهنده‌ی روز مقصده (طبق شماره‌گذاری خودمون).

*

ممکنه براتون عادی‌تر باشه که روزهای هفته رو از ۱ تا ۷ بشمرین، ولی برنامه‌نویس‌ها دوست دارن از صفر بشمارن.

پنج روز بعد از جمعه هم میشه چهارشنبه:

Prelude> mod (6 + 5) 7
4

اگه از rem استفاده کنیم، جوابی می‌گیریم که ظاهراً یکسانه:

Prelude> rem (1 + 23) 7
3

ولی اگه بخوایم بدونیم چند روز قبل چند شنبه بوده، جواب ‏‎mod‎‏ و ‏‎rem‎‏ فرق می‌کنن. ببینیم اگه امروز سه‌شنبه باشه، ۱۲ روز قبلش چند شنبه بوده:

Prelude> mod (3 - 12) 7
5
Prelude> rem (3 - 12) 7
-2

جواب ‏‎mod‎‏ درسته، ولی ‏‎rem‎‏ جوابی که می‌خوایم رو نمیده.

فرق کلیدیِ بین این دو تابع در هسکل (نه همه‌ی زبان‌ها) اینه که جوابِ ‏‎mod‎‏ همیشه با مقسوم‌علیه (مخرج کسر) هم‌علامته، ولی ‏‎rem‎‏ همیشه با مقسوم (صورت کسر) علامتِ یکسان داره:

Prelude> (-5) `mod` 2
1
Prelude> 5 `mod` (-2)
-1
Prelude> (-5) `mod` (-2)
-1

ولی:

Prelude> (-5) `rem` 2
-1
Prelude> 5 `rem` (-2)
1
Prelude> (-5) `rem` (-2)
-1

با کمی تجربه میشه تشخیص داد باید کجا از کدومشون استفاده کرد.

اعداد منفی

به خاطر تعامل پرانتزها، ‏‎currying‎‏، و گرامر ِ میانوندی در هسکل، اعداد منفی شرایط خاصی پیدا می‌کنن.

وقتی فقط یه عدد منفی به تنهایی می‌خواین، کد زیر موردی نداره:

Prelude> -1000
-1000

ولی بعضی مواقع جواب نمیده:

Prelude> 1000 + -9
<interactive>:3:1
    Precedence parsing error
        cannot mix ‘+’ [infixl 6] and
        prefix `-` [infixl 6]
           in the same infix expression

خوشبختانه قبل از اجرای کد، خطا مون پیدا شد. دقت کنین که پیغام خطا از تقدم ایراد گرفته. جمع و تفریق هر دوشون تقدم ِ یکسان (۶) دارن، و GHCi هم فکر می‌کنه که ما می‌خواستیم جمع کنیم بعد تفریق، نه اینکه با یه عددِ منفی جمع کنیم، به همین خاطر نمیدونه چطور تلاقیِ تقدم‌ها رو حل کنه و جواب بده. یه تغییراتی لازمه تا بتونیم اعداد مثبت و منفی رو با هم جمع کنیم:

Prelude> 1000 + (-9)
991

منفی کردن اعداد با یک مِنها (‏‎-‎‏) در هسکل، نوعی شکرِ گرامری ِه. syntaxگرامر، ساختارِ نوشتاریه که باهاش برنامه رو بیان می‌کنیم؛ و شکرِ گرامری راهی برای ساده‌تر کردن این متن‌ها برای خوندن و نوشتن‌ه. شکر گرامری هیچ تأثیری در معنا و مفهوم برنامه‌ها نداره، فقط خوندن یا نوشتن کد رو راحت‌تر می‌کنه، روش حل مسائل با کد رو هم تغییری نمیده. معمولاً اگه در کُدی که به REPL یا کامپایلر میدیم از این نوع شکرها وجود داشته باشه، بعد از پارس شدن، یه تبدیلِ جزئی از فرم کوتاه (یا "شیرین‌تر") به فرم گویاتر روی کد انجام میشه.

در مورد این شکر گرامری، منها می‌تونه دو معنی داشته باشه: یکی اینکه مستعاری از تابعِ ‏‎negate‎‏ باشه، یا اینکه تابعِ تفریق باشه. در زیر، هر دو مثال دقیقاً معادل‌اند، چون ‏‎-‎‏ به ‏‎negate‎‏ ترجمه میشه:

Prelude> 2000 + (-1234)
766

Prelude> 2000 + (negate 1234)
766

ولی اینجا ‏‎-‎‏ برای تفریق استفاده شده:

Prelude> 2000 - 1234
766

خوشبختانه تو هسکل، گرامرِ سربار مثل این زیاد نیست.