۲۶ - ۳بیرون به داخل، داخل به بیرون
زبانهای اکید، از داخل به بیرون محاسبه میکنن؛ زبانهای نااکید مثلِ هسکل، از بیرون به داخل. منظور از بیرون به داخل اینه که محاسبه از بیرونیترین بخشهای بیانیه شروع میشه، و برمبنای اینکه چه مقادیری اجبار میشن به داخل پیش میره. چنین چیزی باعث میشه بسته به ورودیها، ترتیب و چیزهایی که محاسبه میشن تغییر کنه.
این مثال عجیب رو برای بهتر نشون دادن ترتیبِ محاسبه نوشتیم:
possiblyKaboom =
\f -> f fst snd (0, undefined)
-- مقادیر بولیَن در فرمِ لاندا
true :: a -> a -> a
true = \a -> (\b -> a)
false :: a -> a -> a
false = \a -> (\b -> b)
اگه possiblyKaboom
رو به true
اعمال کنیم، f
میشه true
، a
میشه fst
، و b
میشه snd
. از لحاظ مفهومی، با تودرتو کردنِ لانداها و سادهسازیِ از بیرون به داخل، میشه تطبیقهای case، بیانیههای گارد، و بیانیههای if-then-else
رو به همین ترتیب نوشت (البته کامپایلر اینطوری تجزیهشون نمیکنه):
(\f ->
f fst snd (0, undefined))
(\a -> (\b -> a))
(\a -> (\b -> a)) fst snd (0, undefined)
(\b -> fst) snd (0, undefined)
fst (0, undefined)
0
مثال بعدی یه کم به هسکلِ معمولی نزدیکتره، و همون جواب رو میده. اینجا وقتی تابع رو به True
اعمال میکنیم، با case روی مقدارِ True
، مقدارِ اولِ توپل رو برمیگردونه:
possiblyKaboom b =
case b of
True -> fst tup
False -> snd tup
where tup = (0, undefined)
تهی داخلِ توپل ِه، و اون توپل داخلِ لاندایی انقیاد داده شده که با case رو یه مقدارِ بولین، یا مقدارِ اول رو برمیگردونه یا مقدارِ دوم. به خاطرِ اینکه از بیرون محاسبه رو شروع میکنیم، تا زمانی که این تابع فقط به True
اعمال بشه، اون تهی هیچ مشکلی ایجاد نمیکنه. میدونیم هشدارِ واضحیه، اما برنامهای ننویسین که اینوَر اونورِش تهی افتاده باشه.
محاسبهی بیرون-به-داخل، یعنی نه تنها یه سری از بیانیههای تودرتو رو از بیرون شروع به محاسبه میکنیم، بلکه فقط بعضی از بیانیهها رو، اون هم بعضی از اوقات محاسبه میکنیم. در هسکل، بجای اینکه بیانیهها رو در لحظهی ساخته شدن، یا اولین ارجاع محاسبه کنیم، زمانی که بهشون احتیاج داریم محاسبه میکنیم. این یکی از دلایلِ گویاییِ هسکله – میشه به مقادیری که هنوز نساختیم ارجاع داشته باشیم.
این الگو، هم برای دادهسازها و هم برای لانداها صادقه. در فصلِ فولدها تأثیرِ محاسبهی بیرون-به-داخل رو دیدین. دلیل اینکه میتونیم بدون اینکه هیچ کاری به مقادیرِ داخلِ یه لیست داشته باشیم طولش رو پیدا کنیم، بیرون به داخل بودنِ محاسباته. کدِ زیر رو در نظر بگیرین:
-- foldr یکی از تعاریفِ قدیمیِ
foldr k z xs = go xs
where
go [] = z
go (y:ys) = y `k` go ys
c = foldr const 'z' ['a'..'e']
اگه foldr
توی c
رو باز کنیم:
c = const 'z' "abcde" = go "abcde"
where
go [] = 'z'
go ('a':"bcde") = 'a' `const` go "bcde"
-- پس اینجا، اولین قدم در
-- محاسبهی فولد اینطور میشه:
const 'a' (go "bcde")
const x y =
const 'a' (go "bcde") = 'a'
دومین آرگومان و مرحلهی فولد هیچ وقت محاسبه نمیشه:
const 'a' _ = 'a'
حتی اگه مقدارِ بعدی تهی هم باشه اهمیتی نداره:
Prelude> foldr const 'z' ['a', undefined]
'a'
اینجا میشه بیرون-به-داخل بودن رو به چشم دید. تابعِ const
در بیرونیترین جایگاه بود، پس اول محاسبه شد.