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.

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ż

Notatki