تر

ترکیب توابع در Julia

1 تمرین

درباره‌ی ترکیب توابع

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

این کار معمولاً توابع کوچک و ساده‌ی زیادی به جا می‌گذارد که برای انجام کارهای غیرساده باید با هم ترکیب شوند.

یک روش واضح این است که فراخوانی توابع را تودرتو کنیم. مثال زیر خیلی ساختگی است، اما موضوع را روشن می‌کند.

julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

عیب این روش آن است که هرچه تودرتویی عمیق‌تر شود، خوانایی به‌سرعت کم می‌شود.

به روشی ساده‌تر و انعطاف‌پذیرتر نیاز داریم.

ترکیب

این همان تکنیکی است که ریاضیدانان دوستش دارند و Julia شکل نوشتن ریاضی آن را کپی کرده است.

هر تعداد دلخواهی از توابع را می‌توان با عملگرهای ∘ به‌صورت composed به هم ترکیب کرد (این نویسه با تایپ \circ و سپس کلید Tab وارد می‌شود). نتیجه را می‌توان به‌عنوان یک تابع واحد به کار برد.

julia> compfunc = first ∘ titlecase ∘ reverse
first ∘ titlecase ∘ reverse

julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

# alternative syntax, giving the same result
julia> (first ∘ titlecase ∘ reverse).(["my", "test", "strings"])

به چند نکته توجه کنید:

  • توابع آغازین به همان ترتیبی که در حالت تودرتو بودند ظاهر می‌شوند و به ترتیب راست‌به‌چپ اجرا می‌شوند.
  • «پخش» هنگام ترکیب ساده نیست، اما هنگام فراخوانی تابع ترکیب‌شده می‌توان از آن استفاده کرد.

پایپلاین

می‌توان به یک جایگزین به‌عنوان رویکرد برنامه‌نویسان نگاه کرد، نه رویکرد ریاضیدانان.

Pipelines از دیرباز در اسکریپت‌های شل یونیکس به کار می‌رفته‌اند و اخیراً در زبان‌های برنامه‌نویسی جریان اصلی رایج شده‌اند (گاهی F# را پیشگام رواج آن‌ها می‌دانند).

مفهوم پایه این است که با مقداری داده شروع می‌کنید و بعد آن را از میان دنباله‌ای از توابع عبور می‌دهید تا نتیجه را به دست آورید.

عملگر لوله |> است (همان‌طور که در F# و نسخه‌های جدید R)، هرچند Julia نسخه‌ی پخشی .|> را هم دارد.

julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

اکنون اجرا دقیقاً از چپ به راست است و خروجی هر تابع در جهت فلش جریان می‌یابد تا ورودی تابع بعدی شود.

محدودیت‌ها، راه‌های دور زدن و گزینه‌های دیگر

تصادفی نیست که همه‌ی توابعی که برای نشان دادن ترکیب و پایپلاین به کار رفته‌اند، فقط یک آرگومان می‌گیرند.

برخی زبان‌های کاملاً تابعی، آرگومان اول را به تابع هدایت می‌کنند اما اجازه می‌دهند آرگومان‌های دیگری هم در آن گنجانده شوند.

در مقابل، Julia در یک پایپلاین فقط اسم توابع (یا چیزی معادل آن) را انتظار دارد، بدون هیچ آرگومان اضافه‌ای.

دلایل فنی مهمی برای این کار وجود دارد (مربوط به این واقعیت که currying بخش استانداردی از طراحی زبان نیست). بسیاری از کسانی که هیچ درکی از currying ندارند، بهتر است فقط بپذیرند که این محدودیت یک بی‌توجهی سهوی نیست و احتمالاً در نسخه‌های آینده‌ی Julia هم تغییر نمی‌کند.

راه‌های دور زدن

به توابعی با یک آرگومان نیاز داریم که هر کاری را که لازم است انجام دهند. خوشبختانه، تعریف توابع جدید در Julia آسان است.

ساده‌ترین راه این است که از یک anonymous function استفاده کنیم. برای مثال، اگر یک رشته‌ی ورودی داشته باشیم و بخواهیم آن را روی زیرخط‌ها تقسیم کنیم:

julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

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

گذاشتن تابع بی‌نام درون پرانتز در این مورد اختیاری است، اما در حالت کلی روش مفیدی برای کاهش ابهام است.

به همین ترتیب، می‌توانیم یک تابع نام‌دار را در بخش‌های قبلی برنامه بسازیم و هر وقت لازم شد دوباره از آن استفاده کنیم.

Closures فراتر از حیطه‌ی این مفهوم است، اما هر کسی که از زبان‌های دیگر با آن‌ها آشنا باشد، خواهد دید که روش انعطاف‌پذیرتری برای ساختن توابع تک‌آرگومانی ارائه می‌دهند.

julia> function makesplit(sep)
           fs(str) = split(str, sep)
           fs
       end
makesplit (generic function with 1 method)

julia> f_us = makesplit('_')
(::var"#fs#32"{Char}) (generic function with 1 method)

julia> "my_test_strings" |> f_us
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

# alternatively:
julia> "my_test_strings" |> makesplit('_')
3-element Vector{SubString{String}}:
 "my"
 "test"
 "strings"

وقتی makesplit() تعریف شد، می‌توان از آن برای کار با هر جداکننده‌ای استفاده کرد. دقت کنید که makesplit('_') یک فراخوانی تابع است که به تابع دیگری ارزیابی می‌شود و آن تابع به‌نوبه‌ی خود ورودی را از لوله دریافت می‌کند.

اگر این گیج‌کننده به نظر می‌رسد، در ابتدا طبیعی است (اما با تمرین روشن‌تر می‌شود).

گزینه‌های دیگر

بحث‌های طولانی درباره‌ی همه‌کاره‌تر کردن لوله‌ها در Julia پایه انجام شده است، اما پیشنهادهای گوناگون با هم ناسازگارند و توافقی حاصل نشده است.

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

ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

ترکیب توابع را یاد بگیرید