Skład funkcji (informatyka)
W informatyce skład funkcji jest aktem lub mechanizmem łączenia prostych funkcji w celu zbudowania bardziej skomplikowanych . Podobnie jak w zwykłym składaniu funkcji w matematyce , wynik każdej funkcji jest przekazywany jako argument następnej, a wynik ostatniej jest wynikiem całości.
Programiści często stosują funkcje do wyników innych funkcji i pozwalają na to prawie wszystkie języki programowania. W niektórych przypadkach kompozycja funkcji jest interesująca jako funkcja sama w sobie, do późniejszego wykorzystania. Taką funkcję zawsze można zdefiniować, ale języki z funkcjami pierwszej klasy to ułatwiają.
Możliwość łatwego komponowania funkcji zachęca do rozkładania funkcji na czynniki w celu ułatwienia konserwacji i ponownego wykorzystania kodu . Mówiąc bardziej ogólnie, duże systemy można budować, tworząc całe programy.
Mówiąc w skrócie, skład funkcji dotyczy funkcji, które działają na skończonej ilości danych, a każdy krok przetwarza je sekwencyjnie przed przekazaniem do następnego. Funkcje operujące na potencjalnie nieskończonych danych ( strumieniu lub innych kodowanych danych ) są znane jako filtry i zamiast tego są połączone w potok , który jest analogiczny do składania funkcji i może być wykonywany współbieżnie .
Komponowanie wywołań funkcji
Załóżmy na przykład, że mamy dwie funkcje f i g , jak w z = f ( y ) i y = g ( x ) . Złożenie ich oznacza, że najpierw obliczamy y = g ( x ) , a następnie używamy y do obliczenia z = f ( y ) . Oto przykład w języku C :
liczba zmiennoprzecinkowa x , y , z ; // ... y = g ( x ); z = fa ( y );
Kroki można łączyć, jeśli nie nadamy nazwy wynikowi pośredniemu:
z = fa ( sol ( x ));
Pomimo różnic w długości, te dwie implementacje obliczają ten sam wynik. Druga implementacja wymaga tylko jednej linii kodu i jest potocznie określana jako „wysoce złożona” forma. Czytelność, a tym samym łatwość konserwacji, jest jedną z zalet wysoce złożonych formularzy, ponieważ wymagają one mniej wierszy kodu, minimalizując „powierzchnię” programu. DeMarco i Lister empirycznie weryfikują odwrotną zależność między polem powierzchni a podatnością na konserwację. Z drugiej strony, możliwe jest nadużywanie form mocno skomponowanych. Zagnieżdżenie zbyt wielu funkcji może mieć odwrotny skutek, sprawiając, że kod będzie trudniejszy w utrzymaniu.
W języku opartym na stosie kompozycja funkcjonalna jest jeszcze bardziej naturalna: jest wykonywana przez konkatenację i jest zwykle podstawową metodą projektowania programu. Powyższy przykład w Forth :
dziewczyna
Który weźmie wszystko, co było na stosie wcześniej, zastosuj g, następnie f i pozostaw wynik na stosie. Zobacz notację składu przyrostkowego dla odpowiedniej notacji matematycznej.
Nazywanie składania funkcji
Załóżmy teraz, że kombinacja wywołania f() na wyniku g() jest często użyteczna i którą chcemy nazwać foo(), aby była używana jako samodzielna funkcja.
W większości języków możemy zdefiniować nową funkcję zaimplementowaną przez kompozycję. Przykład w C :
float foo ( float x ) { return fa ( g ( x )); }
(długa forma z półproduktami również by zadziałała.) Przykład w Forth :
: fuj gf;
W językach takich jak C jedynym sposobem na utworzenie nowej funkcji jest zdefiniowanie jej w źródle programu, co oznacza, że funkcji nie można tworzyć w czasie wykonywania . Możliwa jest jednak ocena dowolnej kompozycji predefiniowanych funkcji:
#include <stdio.h> typedef int FXN ( int ); int fa ( int x ) { return x + 1 ; } int g ( int x ) { return x * 2 ; } int h ( int x ) { return x -3 ; } wartość całkowita
0
( FXN * fs [], int rozmiar , int x ) { for ( int ja = ; ja < rozmiar ; i ++ ) x = ( * fs [ ja ]) ( x ); zwróć x ; } int main () { // ((6+1)*2)-3 = 11 FXN * arr [] =
0
{ fa , sol , godz }; printf ( "%d \n " , eval ( arr , 3 , 6 )); // ((6-3)*2)+1 = 7 arr [ 2 ] = f ; arr [ ] = h ; printf ( "%d \n " , eval ( arr , 3 , 6
)); }
Skład pierwsza klasa
W funkcjonalnych językach programowania skład funkcji można naturalnie wyrazić jako funkcję lub operator wyższego rzędu . W innych językach programowania można pisać własne mechanizmy wykonywania kompozycji funkcji.
Haskella
W Haskell przykład foo = f ∘ g podany powyżej przyjmuje postać:
foo = f . G
używając wbudowanego operatora kompozycji (.), który można odczytać jako f po g lub g złożone z f .
operator kompozycji ∘ można zdefiniować w Haskell za pomocą wyrażenia lambda :
( . ) :: ( b -> do ) -> ( za -> b ) -> za -> do fa . sol = \ x -> fa ( sol x )
Pierwsza linia opisuje typ (.) - pobiera parę funkcji f , g i zwraca funkcję (wyrażenie lambda w drugiej linii). Zauważ, że Haskell nie wymaga określenia dokładnych typów wejścia i wyjścia f i g; a, b, c i x są symbolami zastępczymi; tylko relacja między f , g (f musi zaakceptować to, co zwraca g). To sprawia, że (.) jest polimorficznym .
Seplenienie
Warianty Lispa , zwłaszcza Scheme , zamienność kodu i danych wraz z traktowaniem funkcji nadają się wyjątkowo dobrze do rekurencyjnej definicji zmiennokształtnego operatora kompozycji.
( zdefiniuj ( komponuj . fs ) ( if ( null? fs ) ( lambda ( x ) x ) ; jeśli nie podano argumentu, oblicza funkcję identyczności ( lambda ( x ) (( car fs ) (( zastosuj komponowanie ( cdr fs ) )) x ))))) ; przykłady ( zdefiniuj (
add-a-bang str ) ( string-append str "!" )) ( zdefiniuj givebang ( skomponuj string-> symbol add-a-bang symbol-> string )) ( givebang 'set ) ; ===> zestaw! ; kompozycja anonimowa (( compose sqrt negate square ) 5 ) ; ===> 0+5i
APL
Wiele dialektów APL ma wbudowaną kompozycję funkcji za pomocą symbolu ∘
. Ta funkcja wyższego rzędu rozszerza kompozycję funkcji na diadyczne zastosowanie funkcji lewej strony tak, że A f∘g B
jest A fg B
.
foo ← fa ∘ sol
Dodatkowo możesz zdefiniować kompozycję funkcji:
o ← { ⍺⍺ ⍵⍵ ⍵ }
W dialekcie, który nie obsługuje definicji wbudowanej za pomocą nawiasów klamrowych, dostępna jest tradycyjna definicja:
∇ r ← ( fa o sol ) x r ← fa sol x ∇
Raku
Raku, podobnie jak Haskell , ma wbudowany operator kompozycji funkcji, główna różnica polega na tym, że jest zapisywany jako ∘
lub o
.
mój & foo = & f ∘ & g ;
Podobnie jak Haskell, możesz sam zdefiniować operatora. W rzeczywistości poniżej znajduje się kod Raku używany do zdefiniowania go w Rakudo .
# implementacja ma tutaj nieco inną linię, ponieważ oszukuje proto subinfix :<∘> (&?, &?) is equiv(&[~]) is assoc<left> { * } multi sub infix :<∘ > ( ) { *. self } # pozwala `[∘] @tablica` działać, gdy `@tablica` jest pusta multi wrostek podrzędny :<∘> (&f) { & f } # pozwala `[∘] @tablica` działać, gdy `@tablica` ma jeden element multi wrostek podrzędny
:<∘> (&f, &g --> Blok) { ( & f ) . liczyć > 1 ?? -> | argumenty { f | g | argumenty } !! -> | argumenty { f g | args } } # alias do pisowni „Texas” (wszystko jest większe, a ASCII w Teksasie) my & infix: <o> : = & wrostek: <∘> ;
Pyton
W Pythonie sposobem na zdefiniowanie składu dowolnej grupy funkcji jest użycie funkcji reduce (użyj functools.reduce w Pythonie 3):
# Dostępne od Pythona w wersji 2.6 z functools import reduce def compose ( * funcs ) -> int : """Utwórz grupę funkcji (f(g(h(...)))) w jedną złożoną funkcję." "" return reduce ( lambda f , g : lambda x : f ( g ( x )), funcs ) # Przykład f =
lambda x : x + 1 g = lambda x : x * 2 h = lambda x : x - 3 # Wywołaj funkcję x=10 : ((x-3)*2)+1 = 15 print ( compose ( f , g , h )( 10 ))
JavaScript
W JavaScript możemy zdefiniować to jako funkcję, która przyjmuje dwie funkcje f i g i tworzy funkcję:
funkcja o ( fa , sol ) { powrót funkcja ( x ) { powrót fa ( sol ( x )); } } // Alternatywnie, użycie operatora rest i wyrażeń lambda w ES2015 const compose = (... fs ) => ( x ) => fs . zmniejszPrawo (( acc , f ) => fa ( acc ), x )
C#
W języku C# możemy zdefiniować to jako funkcję Func, która przyjmuje dwie funkcje f i g i tworzy funkcję Func:
// Przykład wywołania: // var c = Compose(f, g); // // Func<int, bool> g = _ => ... // Func<bool, string> f = _ => ... Func < T , T > Compose < T >( params Func < T , T >[] xs ) => xs . Agregat (( accum , item ) => x => accum ( element ( x )));
Rubin
Języki takie jak Ruby pozwalają samodzielnie skonstruować operator binarny:
class Proc def compose ( inne_fn ) -> ( * as ) { inne_fn . call ( call ( * as )) } end alias_method :+ , :compose end f = -> ( x ) { x * 2 } g = -> ( x ) { x
** 3 } ( fa + g ) . zadzwoń ( 12 ) # => 13824
Jednak w Ruby 2.6 wprowadzono natywny operator kompozycji funkcji:
fa = proces { | x | x + 2 } g = proc { | x | x * 3 } ( fa << g ) . zadzwoń ( 3 ) # -> 11; identyczny z f(g(3)) ( f >> g ) . zadzwoń ( 3 ) # -> 15; identyczny z g(f(3))
Ankieta badawcza
Pojęcia kompozycji, w tym zasady kompozycyjności i kompozycyjności , są tak wszechobecne, że wiele nurtów badań ewoluowało oddzielnie. Poniżej znajduje się próbka rodzaju badań, w których pojęcie kompozycji jest centralne.
- Steele (1994) bezpośrednio zastosował kompozycję funkcji do zbioru bloków konstrukcyjnych znanych jako „ monady ” w języku programowania Haskell .
- Meyer (1988) odniósł się do problemu ponownego wykorzystania oprogramowania w kategoriach możliwości komponowania.
- Abadi i Lamport (1993) formalnie zdefiniowali regułę sprawdzającą dla kompozycji funkcjonalnej, która zapewnia bezpieczeństwo i żywotność programu.
- Kracht (2001) zidentyfikował wzmocnioną formę kompozycyjności, umieszczając ją w systemie semiotycznym i stosując do problemu niejednoznaczności strukturalnej często spotykanej w lingwistyce komputerowej .
- van Gelder i Port (1993) zbadali rolę kompozycyjności w analogowych aspektach przetwarzania języka naturalnego.
- Według recenzji przeprowadzonej przez Gibbonsa (2002) , formalne traktowanie kompozycji leży u podstaw walidacji składania komponentów w wizualnych językach programowania, takich jak IBM Visual Age dla języka Java .
Kompozycja na dużą skalę
Całe programy lub systemy można traktować jako funkcje, które można łatwo komponować, jeśli ich wejścia i wyjścia są dobrze zdefiniowane. Rurociągi umożliwiające łatwe komponowanie filtrów odniosły taki sukces, że stały się wzorcem projektowym systemów operacyjnych.
Imperatywne procedury ze skutkami ubocznymi naruszają przejrzystość referencyjną i dlatego nie można ich czysto komponować. Jeśli jednak weźmie się pod uwagę „stan świata” przed i po uruchomieniu kodu jako jego dane wejściowe i wyjściowe, otrzyma się czystą funkcję. Złożenie takich funkcji odpowiada uruchamianiu procedur jedna po drugiej. Formalizm monady wykorzystuje tę ideę do włączenia efektów ubocznych i wejścia/wyjścia (I/O) do języków funkcjonalnych.
Zobacz też
- Curry
- Dekompozycja funkcjonalna
- Dziedziczenie implementacji
- Semantyka dziedziczenia
- Iteracja
- Rurociąg (Unix)
- Zasada kompozycyjności
- Wirtualne dziedziczenie
Notatki
- Abadi, Martín ; Lamport, Leslie (1993), „Komponowanie specyfikacji” (PDF) , ACM Transactions on Programming Languages and Systems , 15 (1): 73–132, doi : 10.1145/151646.151649 .
- Cox, Brad (1986), Programowanie obiektowe, podejście ewolucyjne , Reading, MA: Addison-Wesley, ISBN 978-0-201-54834-1 .
- Daume, Hal, III, jeszcze jeden samouczek Haskella .
- DeMarco, Tom ; Lister, Tim (1995), „Rozwój oprogramowania: stan techniki a stan praktyki”, w: DeMarco, Tom (red.), Why Does Software Cost So Much i inne zagadki ery informacyjnej , Nowy Jork, NY: Dorset House, ISBN 0-932633-34-X .
- van Gelder, Tymoteusz ; Port, Robert (1993), „Poza symboliką: prolegomena do kamasutry kompozycyjności”, w: Honavar, Vasant ; Uhr, Leonard (red.), Przetwarzanie symboli i modele koneksjonistyczne w sztucznej inteligencji i poznaniu: kroki w kierunku integracji , prasa akademicka .
- Gibbons, Jeremy (2002), Arbab, Farhad; Talcott, Carolyn (red.), Proc. 5th International Conference on Cooperation Models and Languages (PDF) , Notatki z wykładów z informatyki, tom. 2315, Springer-Verlag, s. 339–350, doi : 10.1007/3-540-46000-4\_18 .
- Korn, Henryk; Liberi, Albert (1974), Elementarne podejście do funkcji , Nowy Jork, NY: McGraw-Hill, ISBN 0-07-035339-5 .
- Kracht, Marcus (2001), „Ścisła kompozycyjność i gramatyki ruchu dosłownego”, Proc. 3rd International Conference on Logical Aspects of Computational Linguistics , Notatki z wykładów z informatyki, tom. 2014, Springer-Verlag, s. 126–143, doi : 10.1007/3-540-45738-0_8 .
- Meyer, Bertrand (1988), Obiektowa konstrukcja oprogramowania , Nowy Jork, NY: Prentice Hall, s. 13–15, ISBN 0-13-629049-3 .
- Miller, George A. (1956), „Magiczna liczba siedem plus minus dwa: pewne ograniczenia naszej zdolności przetwarzania informacji” , Psychological Review , 63 (2): 81–97, doi : 10.1037/h0043158 , hdl : 11858/00-001M-0000-002C-4646-B , PMID 13310704 , zarchiwizowane z oryginału w dniu 19.06.2010 , pobrane 02.05.2010 .
- Pierce, Benjamin C.; Turner, David N. (2000), „Pict: Język programowania oparty na rachunku pi-calculus”, Dowód, język i interakcja: eseje na cześć Robina Milnera , Foundations Of Computing Series, Cambridge, MA: MIT Press, s. 455–494, ISBN 0-262-16188-5 .
- Raymond, Eric S. (2003), „1.6.3 Reguła kompozycji: projektowanie programów do łączenia z innymi programami” , The Art of Unix Programming , Addison-Wesley, s. 15–16, ISBN 978-0-13- 142901-7 .
- Steele, Guy L., Jr. (1994), „Budowanie tłumaczy przez komponowanie monad” , Proc. 21. Sympozjum ACM na temat zasad języków programowania , s. 472–492, doi : 10.1145/174675.178068 .