ترکیب تابع (علوم کامپیوتر)
در علوم کامپیوتر ، ترکیب تابع یک عمل یا مکانیزم برای ترکیب توابع ساده برای ساخت توابع پیچیده تر است. مانند ترکیب معمول توابع در ریاضیات ، نتیجه هر تابع به عنوان آرگومان تابع بعدی و حاصل آخرین تابع نتیجه کل است.
برنامه نویسان اغلب توابعی را برای نتایج سایر توابع اعمال می کنند و تقریباً همه زبان های برنامه نویسی این امکان را دارند. در برخی موارد، ترکیب توابع به عنوان یک تابع در نوع خود جالب است که بعدا مورد استفاده قرار می گیرد. چنین تابعی همیشه قابل تعریف است، اما زبان هایی با توابع درجه یک آن را آسان تر می کنند.
توانایی نوشتن آسان توابع، توابع فاکتورگیری (تجزیه) را برای نگهداری و استفاده مجدد از کد تشویق می کند. به طور کلی، سیستم های بزرگ ممکن است با نوشتن برنامه های کامل ساخته شوند.
به بیان محدود، ترکیب تابع برای توابعی اعمال می شود که بر روی مقدار محدودی از داده ها کار می کنند، هر مرحله قبل از تحویل به مرحله بعدی، آن را به صورت متوالی پردازش می کند. توابعی که بر روی دادههای بالقوه بینهایت کار میکنند (یک جریان یا دیگر دادههای کد ) به عنوان فیلتر شناخته میشوند و در عوض در یک خط لوله متصل میشوند که مشابه ترکیب تابع است و میتواند همزمان اجرا شود.
تنظیم فراخوانی تابع
[ویرایش]برای مثال، فرض کنید دو تابع f و g داریم، مانند z = f(y) و y = g(x) . نوشتن آنها به این معنی است که ابتدا y = g(x) را محاسبه می کنیم, و سپس y برای محاسبه z = f(y) استفاده می کنیم . این مثال در زبان C است:
foo←f∘g
اگر به نتیجه میانی نامی ندهیم، مراحل را می توان ترکیب کرد:
z = f(g(x));
با وجود تفاوت در طول، این دو پیاده سازی نتیجه یکسانی را محاسبه می کنند. اجرای دوم تنها به یک خط کد نیاز دارد و به صورت محاوره ای به عنوان فرم "خیلی ترکیب شده" نامیده می شود. خوانایی و در نتیجه قابلیت نگهداری یکی از مزیتهای فرمهای بسیار ترکیبی است، زیرا به خطوط کد کمتری نیاز دارند و «مساحت سطح» برنامه را به حداقل میرسانند. [۱] DeMarco و Lister به طور تجربی رابطه معکوس بین سطح و قابلیت نگهداری را تأیید می کنند. [۲] از سوی دیگر، ممکن است استفاده بیش از حد از فرم های با ترکیب بالا امکان پذیر باشد. تودرتویی از توابع بیش از حد ممکن است اثر معکوس داشته باشد و کد را کمتر قابل نگهداری کند.
در یک زبان مبتنی بر پشته ، ترکیب عملکردی حتی طبیعی تر است: این ترکیب توسط الحاق انجام می شود و معمولاً روش اصلی طراحی برنامه است. مثال بالا در Forth :
g f
که هر آنچه قبلاً روی پشته بود را می گیرد، g و سپس f را اعمال کرده و نتیجه را روی پشته بگذارید. برای نماد ریاضی مربوطه، نماد ترکیب پسوند را ببینید.
نامگذاری ترکیب توابع
[ویرایش]حال فرض کنید ترکیب فراخوانی f() بر روی نتیجه g() اغلب مفید است و میخواهیم آن را foo() نامگذاری کنیم تا به تنهایی به عنوان یک تابع استفاده شود.
در اکثر زبان ها، ما می توانیم یک تابع جدید را تعریف کنیم که توسط ترکیب پیاده سازی شده است. مثال در C :
float foo(float x) {
return f(g(x));
}
(شکل طولانی با واسطه ها نیز کار می کند.) مثال در Forth :
: foo g f ;
در زبان هایی مانند C ، تنها راه برای ایجاد یک تابع جدید، تعریف آن در منبع برنامه است، به این معنی که توابع را نمی توان در زمان اجرا ترکیب کرد. با این حال، ارزیابی یک ترکیب دلخواه از توابع از پیش تعریف شده ممکن است:
#include <stdio.h>
typedef int FXN(int);
int f(int x) { return x+1; }
int g(int x) { return x*2; }
int h(int x) { return x-3; }
int eval(FXN *fs[], int size, int x)
{
for (int i=0; i<size; i++) x = (*fs[i])(x);
return x;
}
int main()
{
// ((6+1)*2)-3 = 11
FXN *arr[] = {f,g,h};
printf("%d\n", eval(arr, 3, 6));
// ((6-3)*2)+1 = 7
arr[2] = f; arr[0] = h;
printf("%d\n", eval(arr, 3, 6));
}
ترکیب بندی درجه یک
[ویرایش]در زبان های برنامه نویسی تابعی ، ترکیب تابع را می توان به طور طبیعی به عنوان یک تابع یا عملگر درجه بالاتر بیان کرد. در سایر زبان های برنامه نویسی می توانید مکانیسم های خود را برای انجام ترکیب تابع بنویسید.
هاسکل
[ویرایش]در Haskell ، مثال foo = f ∘ g داده شده در بالا تبدیل می شود:
foo = f . g
با استفاده از عملگر ترکیب داخلی (.) که می تواند به صورت f بعد از g یا g تشکیل شده با f خوانده شود.
عملگر ترکیب ∘ خود را می توان در Haskell با استفاده از عبارت lambda تعریف کرد:
(.) :: (b -> c) -> (a -> b) -> a -> c
f . g = \x -> f (g x)
خط اول نوع (.) را توصیف می کند - یک جفت تابع f, g را می گیرد و یک تابع را برمی گرداند (عبارت لامبدا در خط دوم). توجه داشته باشید که Haskell نیازی به مشخصات دقیق انواع ورودی و خروجی f و g ندارد. a، b، c و x جایگاهبانها هستند. فقط رابطه بین f, g مهم است (f باید آنچه را که g برمی گرداند بپذیرد). این باعث می شود (.) یک عملگر چند شکلی باشد .
لیسپ
[ویرایش]انواع Lisp ، به ویژه Scheme ، قابلیت تعویض کد و داده به همراه پردازش توابع، خود را بسیار مناسب برای تعریف بازگشتی از یک عملگر ترکیبی متغیر میسازد.
(define (compose . fs)
(if (null? fs) (lambda (x) x) ; if no argument is given, evaluates to the identity function
(lambda (x) ((car fs) ((apply compose (cdr fs)) x)))))
; examples
(define (add-a-bang str)
(string-append str "!"))
(define givebang
(compose string->symbol add-a-bang symbol->string))
(givebang 'set) ; ===> set!
; anonymous composition
((compose sqrt - sqr) 5) ; ===> 0+5i
APL
[ویرایش]
بسیاری از گویشهای APL در ترکیب تابع با استفاده از نماد ∘ ساخته شدهاند. این تابع مرتبه بالاتر ترکیب تابع را به کاربرد دوتایی تابع سمت چپ گسترش می دهد به طوری که A f∘g B A fg B باشد.
foo←f∘g
علاوه بر این، می توانید ترکیب تابع را تعریف کنید:
o←{⍺⍺ ⍵⍵ ⍵}
در لهجه ای که از تعریف درون خطی با استفاده از پرانتز پشتیبانی نمی کند، تعریف سنتی موجود است:
∇ r←(f o g)x
r←f g x
∇
راکو
[ویرایش]Raku مانند Haskell دارای یک عملگر ترکیب تابع داخلی است، تفاوت اصلی این است که به صورت ∘ یا o نوشته می شود.
my &foo = &f ∘ &g;
همچنین مانند Haskell شما می توانید اپراتور را خودتان تعریف کنید. در واقع کد Raku مورد استفاده برای تعریف آن در پیاده سازی Rakudo است.
Nim
[ویرایش]
Nim از نحو فراخوانی تابع یکنواخت پشتیبانی می کند که امکان ترکیب تابع دلخواه را از طریق نحو متد فراهم می کند . اپراتور [۳]
func foo(a: int): string = $a
func bar(a: string, count: int): seq[string] =
for i in 0 ..< count:
result.add(a)
func baz(a: seq[string]) =
for i in a:
echo i
# equivalent!
echo foo(5).bar(6).baz()
echo baz(bar(6, foo(5)))
پایتون
[ویرایش]در پایتون ، راهی برای تعریف ترکیب برای هر گروه از توابع، استفاده از تابع کاهش است (از functools.reduce در پایتون استفاده کنید. 3):
# Available since Python v2.6
from functools import reduce
from typing import Callable
def compose(*funcs) -> Callable[[int], int]:
"""Compose a group of functions (f(g(h(...)))) into a single composite func."""
return reduce(lambda f, g: lambda x: f(g(x)), funcs)
# Example
f = lambda x: x + 1
g = lambda x: x * 2
h = lambda x: x - 3
# Call the function x=10 : ((x-3)*2)+1 = 15
print(compose(f, g, h)(10))
جاوا اسکریپت
[ویرایش]در جاوا اسکریپت می توانیم آن را به عنوان تابعی تعریف کنیم که دو تابع f و g را می گیرد و یک تابع تولید می کند:
function o(f, g) {
return function(x) {
return f(g(x));
}
}
// Alternatively, using the rest operator and lambda expressions in ES2015
const compose = (...fs) => (x) => fs.reduceRight((acc, f) => f(acc), x)
سی شارپ
[ویرایش]در سی شارپ می توانیم آن را به عنوان یک متد Extension تعریف کنیم که Funcs f و g را می گیرد و یک Func جدید تولید می کند:
// Call example:
// var c = f.ComposeWith(g);
//
// Func<int, bool> g = _ => ...
// Func<bool, string> f = _ => ...
public static Func<T1, T3> ComposeWith<T1, T2, T3>(this Func<T2, T3> f, Func<T1, T2> g) => x => f(g(x));
روبی
[ویرایش]زبانهایی مانند Ruby به شما امکان میدهند خودتان یک عملگر باینری بسازید:
class Proc
def compose(other_fn)
->(*as) { other_fn.call(call(*as)) }
end
alias_method :+, :compose
end
f = ->(x) { x * 2 }
g = ->(x) { x ** 3 }
(f + g).call(12) # => 13824
با این حال، یک عملگر ترکیب تابع بومی در Ruby 2.6 معرفی شد: [۴]
f = proc{|x| x + 2}
g = proc{|x| x * 3}
(f << g).call(3) # -> 11; identical to f(g(3))
(f >> g).call(3) # -> 15; identical to g(f(3))
نظرسنجی پژوهشی
[ویرایش]مفاهیم ترکیب، از جمله اصل ترکیبپذیری و ترکیبپذیری ، آنقدر در همه جا وجود دارند که رشتههای تحقیقاتی متعددی به طور جداگانه تکامل یافتهاند. در زیر نمونهای از پژوهشهایی است که مفهوم ترکیب در آن محوریت دارد.
- استیل (1994) به طور مستقیم ترکیب عملکرد را به جمع آوری بلوک های ساختمانی که به عنوان "موناد" در زبان برنامه نویسی هاسکل شناخته می شوند اعمال می کند.
- مایر (1988) مشکل استفاده مجدد نرم افزار را از نظر ترکیب پذیری حل کرد.
- ابادی و لامپورت (1993) به طور رسمی یک قانون اثبات برای ترکیب عملکردی را تعریف کردند که ایمنی و زندگی یک برنامه را تضمین می کند.
- کراخت (2001) یک شکل تقویت شده از ترکیب پذیری را با قرار دادن آن در یک سیستم سیمویتیک و اعمال آن به مشکل عدم وضوح ساختاری که اغلب در زبان شناسی محاسباتی رخ می دهد، شناسایی کرد.
- (van Gelder و Port 1993) نقش ترکیب پذیری را در جنبه های آنالوگ پردازش زبان طبیعی بررسی کرد.
- بر اساس بررسی (Gibbons 2002) ، درمان رسمی ترکیب پایه اعتبار اجزای اجزای اجسام در زبان های برنامه نویسی بصری مانند IBM Visual Age برای زبان جاوا است.
ترکیب در مقیاس بزرگ
[ویرایش]کل برنامهها یا سیستمها را میتوان بهعنوان توابعی در نظر گرفت که در صورتی که ورودیها و خروجیهای آنها به خوبی تعریف شده باشند، میتوانند به راحتی ترکیب شوند. [۵] خطوط لوله ای که امکان ترکیب آسان فیلترها را فراهم می کردند، چنان موفق بودند که به الگوی طراحی سیستم عامل تبدیل شدند.
رویههای ضروری با عوارض جانبی شفافیت ارجاعی را نقض میکنند و بنابراین کاملاً قابل ترکیب نیستند. با این حال، اگر "وضعیت جهان" قبل و بعد از اجرای کد را به عنوان ورودی و خروجی آن در نظر بگیریم، یک تابع تمیز دریافت میکنیم. ترکیب چنین توابعی با اجرای رویه ها یکی پس از دیگری مطابقت دارد. فرمالیسم موناد از این ایده برای ترکیب عوارض جانبی و ورودی/خروجی (I/O) در زبان های کاربردی استفاده می کند.
همچنین ببینید
[ویرایش]- Currying
- Functional decomposition
- [۱]ارث پیاده سازی
- Inheritance semantics
- Iteratee
- [۲]خط لوله (یونیکس)
- Principle of compositionality
- Virtual inheritance
یادداشت ها
[ویرایش]- ↑ (Cox 1986), pp. 15–17
- ↑ (DeMarco و Lister 1995), pp. 133–135.
- ↑ "Nim Manual: Method call syntax". nim-lang.org. Retrieved 2023-08-17.
- ↑ "Ruby 2.6.0 Released". www.ruby-lang.org. Retrieved 2019-01-04.
- ↑ (Raymond 2003)