یک تابع زمانی «بازگشتی دنبالهای» است که آخرین کاری که در آن اجرا میشود، فراخوانی خودش باشد.
هر بار که تابعی فراخوانی میشود، یک «قاب پشته» به همراه متغیرهای محلی و آرگومانهایش روی پشتهی فراخوانی تابع قرار میگیرد. وقتی تابعی برگردد، آن قاب پشته از پشته حذف میشود.
توابع بازگشتی دنبالهای امکان بهینهسازی فراخوانی دنبالهای (یا حذف فراخوانی دنبالهای) را فراهم میکنند. این یک بهینهسازی است که اجازه میدهد فراخوانی بعدی تابع، آخرین قاب پشته را دوباره به کار بگیرد، آن هم وقتی تضمین شده باشد که تابع قبلی دیگر به آن نیازی ندارد. این کار نگرانیهای مربوط به سرریز شدن پشتهی فراخوانی تابع را کاهش میدهد؛ وضعیتی که در آن آنقدر قاب روی پشتهی فراخوانی تابع هست که دیگر حافظهای برای ساختن قاب دیگری باقی نمیماند.
تحت شرایطی خاص، کامپایلر Elm هنگام کامپایل به JavaScript میتواند بهطور خودکار بهینهسازی فراخوانی دنبالهای را انجام دهد.
این بهینهسازی برای یک تابع بازگشتی زمانی میتواند رخ دهد که آخرین عملیات در یک شاخه، با فراخوانی خودِ تابع در قالب یک اعمال تابع ساده انجام شده باشد. بیایید چند مثال را ببینیم:
factorial : Int -> Int
factorial n =
if n <= 1 then
n
else
n * factorial (n-1)
پیادهسازی بالا بازگشتی دنبالهای نیست، چون آخرین عملیات در شاخهی else یک ضرب n * است.
factorial : Int -> Int
factorial n =
factorialHelper n n
factorialHelper : Int -> Int -> Int
factorialHelper n resultSoFar =
if n <= 1 then
resultSoFar
else
factorialHelper (n-1) (n * resultSoFar)
پیادهسازی بالا بازگشتی دنبالهای است و بهینه میشود، چون آخرین عملیات در شاخهی else فراخوانی خودِ factorialHelper است.
این کار برای تابعی با امضای نوع Int -> Int ممکن نبود. در عمل بهینهسازی فراخوانی دنبالهای اغلب با تعریف توابع کمکی به دست میآید.
پیپر پایپز پرشوری است.
هیچکس نمیداند او پایپزی را به خاطر اسمش انتخاب کرده یا اسمش را برای هماهنگی با سرگرمیاش عوض کرده است. در نگاه اول، مورد دوم چندان محتمل به نظر نمیرسد، اما ببینید، پیپر بهشدت شیفتهی پای است. او همیشه در آشپزخانه ور میرود، دستورهای پختش را دستکاری میکند و مهارتش را بهبود میبخشد، و همین باعث خوشحالی فراوان دوستانش است.
آخرین علاقهاش؟ پختن پایهایی که تا حد ممکن گرد باشند، تا مرز کمال ریاضی، با کمک عدد محبوبش، حدس زدید: π.
پیپر فرمول دلپذیری برای محاسبهی π بهصورت تکراری پیدا کرده است، به اسم تبدیل همگرایی نیوتون/اویلر:
π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!
به پیپر کمک کنید تا پای بینقص ریاضیاش را با محاسبهی π بپزد.
اول کمی گرم کنیم.
عملگر فاکتوریل، که معمولاً با ! نوشته میشود، اینگونه تعریف میشود:
0! = 1
n! = 1 * 2 * 3 * ... * n
تابع factorial را تعریف کنید که فاکتوریل را بهصورت «بازگشتی دنبالهای» محاسبه کند.
factorial 4
-- 24
عملگر فاکتوریل دوگانه، که معمولاً با !! نوشته میشود، اینگونه تعریف میشود:
0!! = 1
n!! = 1 * 3 * 5 * ... * n (for odd n)
n!! = 2 * 4 * 6 * ... * n (for even n)
تابع doubleFactorial را تعریف کنید که فاکتوریل را بهصورت بازگشتی دنبالهای محاسبه کند.
factorial 5
-- 15
factorial 6
-- 48
تابع pipersPi را تعریف کنید که π را با استفاده از تعداد مشخصی جمله از فرمول تبدیل همگرایی نیوتون/اویلر، بهصورت بازگشتی دنبالهای تقریب میزند.
π / 2 = sum for k from 0 to infinity of ( k! ) / ( 2 * k + 1 )!!
بیایید جملهی اول را با هم حساب کنیم.
برای حد بالای 0 (به جای بینهایت)، به این نتیجه میرسیم:
π / 2 ≈ Sum for k from 0 to 0 of ( k! ) / ( 2 * k + 1 )!!
π / 2 ≈ ( 0! ) / ( 2 * 0 + 1 )!!
π / 2 ≈ 0! / 0!!
π / 2 ≈ 1 / 1
π / 2 ≈ 1
π ≈ 2
هر جملهی اضافه تقریب را بهتر میکند.
pipersPi 0
-- 2.0
pipersPi 1
-- 2.6666666