پرش به محتویات

نوع داده در تابع

وقتی به بسته‌هایی مانند elm/core و elm/html نگاه می‌کنید، حتما توابعی با چندین پیکان خواهید دید. برای نمونه:

String.repeat : Int -> String -> String
String.join : String -> List String -> String

چرا این همه پیکان؟ اینجا چه خبر است؟!

پرانتزهای پنهان

وقتی همه پرانتزها را ببینید، موضوع کمی روشن می‌شود. برای نمونه، نوشتن تابع String.repeat به این شکل نیز معتبر است:

String.repeat : Int -> (String -> String)

این یک تابع است که یک مقدار Int می‌گیرد و سپس یک تابع دیگر تولید می‌کند. بیایید این را در عمل ببینیم:

> String.repeat
<function> : Int -> String -> String

> String.repeat 4
<function> : String -> String

> String.repeat 4 "ha"
"hahahaha" : String

> String.join
<function> : String -> List String -> String

> String.join "|"
<function> : List String -> String

> String.join "|" ["red","yellow","green"]
"red|yellow|green" : String

بطور نظری، هر تابع یک آرگومان می‌گیرد. ممکن است تابع دیگری را برگرداند که یک آرگومان می‌گیرد. در نهایت، دیگر تابعی بر نمی‌گردد.

همیشه می‌توانیم پرانتزها را بگذاریم تا نشان دهیم واقعا چه اتفاقی در حال وقوع است، اما وقتی چندین آرگومان دارید، این کار به شدت دشوار می‌شود. این همان منطقی است که پشت نوشتن عبارت 4 * 2 + 5 * 3 بجای عبارت (4 * 2) + (5 * 3) وجود دارد. البته که یادگیری آن زمان بیشتری می‌برد، اما آنقدر رایج است که ارزشش را دارد.

بسیار خوب، اما هدف از این ویژگی در وهله اول چیست؟ چرا تابع را به صورت (Int, String) ننویسیم و همه آرگومان‌ها را یکجا ندهیم؟

فراخوانی جزیی

استفاده از تابع List.map در برنامه‌های Elm بسیار رایج است:

List.map : (a -> b) -> List a -> List b

این تابع دو آرگومان می‌گیرد: یک تابع و یک لیست. از آنجا، هر عنصر لیست را با آن تابع، دگرگون می‌سازد. در ادامه، چند نمونه آورده شده است:

List.map String.reverse ["part","are"] == ["trap","era"]
List.map String.length ["part","are"] == [4,3]

به یاد دارید که نوع داده String.repeat 4 به تنهایی String -> String بود؟ خوب، به این معنی است که می‌توانیم بگوییم:

List.map (String.repeat 2) ["ha","choo"] == ["haha","choochoo"]

عبارت (String.repeat 2) یک تابع از نوع String -> String است، بنابراین می‌توانیم بطور مستقیم از آن استفاده کنیم. نیازی به استفاده از (\str -> String.repeat 2 str) نیست.

Elm از این قاعده در سراسر اکوسیستم خود استفاده می‌کند که ساختار داده همیشه آخرین آرگومان است. یعنی توابع معمولا با این کاربرد طراحی می‌شوند و این یک تکنیک نسبتا رایج است.

مهم است به یاد داشته باشید که فراخوانی جزیی می‌تواند بیش از حد استفاده شود! گاهی اوقات راحت و واضح است، اما معتقدم بهترین استفاده از آن در حد اعتدال است. بنابراین، همیشه توصیه می‌کنم وقتی اوضاع کمی پیچیده می‌شود، توابع کمکی سطح بالا را جداسازی کنید. به این ترتیب نام آن واضح، آرگومان‌ها نام‌گذاری و آزمایش عملکرد تابع آسان می‌شود. در نمونه قبل، این توصیه به معنای ایجاد چنین کدی است:

-- List.map reduplicate ["ha","choo"]

reduplicate : String -> String
reduplicate string =
  String.repeat 2 string

این مورد واقعا ساده است، اما (۱) اکنون واضح‌تر است که به پدیده زبانی Reduplication علاقه‌مند هستم و (۲) افزودن منطق جدید به تابع reduplicate به راحتی امکان‌پذیر خواهد بود. شاید بخواهم در یک نقطه از shm-reduplication پشتیبانی کنم؟

به عبارت دیگر، اگر فراخوانی جزیی طولانی می‌شود، آن را به یک تابع کمکی تبدیل کنید. و اگر چند خطی است، باید قطعا به یک تابع کمکی سطح بالا تبدیل شود! این توصیه همچنین درباره استفاده از توابع ناشناس نیز صدق می‌کند.

یادداشت

اگر با استفاده از این توصیه به "بسیاری" از توابع برخورد کردید، توصیه می‌کنم از کامنتی مانند REDUPLICATION -- برای ارایه یک نمای کلی از پنج یا ده تابع بعدی استفاده کنید. همان تکنیک قدیمی! این کار را با کامنت UPDATE -- و VIEW -- در نمونه‌های قبلی نشان داده‌ام، اما این یک تکنیک عمومی است که در تمام کدهایم استفاده می‌کنم. اگر نگران این هستید که فایل‌ها با این توصیه خیلی طولانی شوند، پیشنهاد می‌کنم ارایه The Life of a File را مشاهده کنید!

پایپ‌لاین

Elm یک عملگر پایپ دارد که به فراخوانی جزیی تابع، وابسته است. فرض کنید یک تابع sanitize برای تبدیل ورودی کاربر به اعداد صحیح داریم:

-- BEFORE

sanitize : String -> Maybe Int
sanitize input =
  String.toInt (String.trim input)

می‌توانیم آن را به این شکل بازنویسی کنیم:

-- AFTER

sanitize : String -> Maybe Int
sanitize input =
  input
    |> String.trim
    |> String.toInt

در این "پایپ‌لاین" ابتدا ورودی به تابع String.trim ارسال، سپس خروجی آن به ورودی تابع String.toInt منتقل می‌شود.

این تکنیک جالبی است زیرا اجازه می‌دهد که خواندن به صورت "چپ به راست" انجام شود که بسیاری از افراد آن را دوست دارند، اما پایپ‌لاین می‌تواند بیش از حد استفاده شود! وقتی سه یا چهار مرحله دارید، با جداسازی یک تابع کمکی سطح بالا، معمولا کد واضح‌تر می‌شود. اکنون تابع تبدیل یا دگرگون‌ساز دارای نام می‌شود، آرگومان‌ها نام‌گذاری شده‌اند و یک نشانه‌گذاری نوع داده وجود دارد. این روش، به مستندسازی بهتر کد کمک می‌کند. خودتان و هم‌تیمی‌هایتان در آینده از آن قدردانی خواهید کرد! همچنین، آزمایش منطق کد نیز آسان‌تر می‌شود.

یادداشت

من حالت BEFORE را ترجیح می‌دهم، اما شاید فقط به این دلیل باشد که برنامه‌نویسی تابعی را در زبان‌هایی بدون پایپ‌لاین یاد گرفتم!