۲۶ - ۳بیرون به داخل، داخل به بیرون

زبان‌های اکید، از داخل به بیرون محاسبه می‌کنن؛ زبان‌های نااکید مثلِ هسکل، از بیرون به داخل. منظور از بیرون به داخل اینه که محاسبه از بیرونی‌ترین بخش‌های بیانیه شروع میشه، و برمبنای اینکه چه مقادیری اجبار میشن به داخل پیش میره. چنین چیزی باعث میشه بسته به ورودی‌ها، ترتیب و چیزهایی که محاسبه میشن تغییر کنه.

این مثال عجیب رو برای بهتر نشون دادن ترتیبِ محاسبه نوشتیم:

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‎‏ در بیرونی‌ترین جایگاه بود، پس اول محاسبه شد.