stringtranslate.com

Composición de funciones (informática)

En informática , la composición de funciones es un acto o mecanismo para combinar funciones simples para construir otras más complicadas. Al igual que la composición habitual de funciones en matemáticas , el resultado de cada función se pasa como argumento de la siguiente, y el resultado de la última es el resultado del conjunto.

Los programadores aplican frecuentemente funciones a los resultados de otras funciones, y casi todos los lenguajes de programación lo permiten. En algunos casos, la composición de funciones resulta interesante como función en sí misma, para ser utilizada posteriormente. Siempre se puede definir una función de este tipo, pero los lenguajes con funciones de primera clase lo hacen más sencillo.

La capacidad de componer funciones fácilmente fomenta la factorización (descomposición) de funciones para facilitar el mantenimiento y la reutilización del código . En términos más generales, los sistemas grandes se pueden construir componiendo programas completos.

En términos estrictos, la composición de funciones se aplica a funciones que operan sobre una cantidad finita de datos, y cada paso los procesa secuencialmente antes de pasarlos al siguiente. Las funciones que operan sobre datos potencialmente infinitos (un flujo u otros codatos ) se conocen como filtros y, en cambio, están conectadas en una tubería , que es análoga a la composición de funciones y puede ejecutarse simultáneamente .

Composición de llamadas a funciones

Por ejemplo, supongamos que tenemos dos funciones f y g , como z = f ( y ) e y = g ( x ) . Para componerlas, primero calculamos y = g ( x ) y luego usamos y para calcular z = f ( y ) . Aquí está el ejemplo en lenguaje C :

flotante x , y , z ; // ... y = g ( x ); z = f ( y );       

Los pasos se pueden combinar si no le damos un nombre al resultado intermedio:

z = f ( g ( x ));  

A pesar de las diferencias de longitud, estas dos implementaciones calculan el mismo resultado. La segunda implementación requiere sólo una línea de código y se la conoce coloquialmente como una forma "altamente compuesta". La legibilidad y, por lo tanto, la facilidad de mantenimiento es una ventaja de las formas altamente compuestas, ya que requieren menos líneas de código, lo que minimiza el "área de superficie" de un programa. [1] DeMarco y Lister verifican empíricamente una relación inversa entre el área de superficie y la facilidad de mantenimiento. [2] Por otro lado, puede ser posible abusar de las formas altamente compuestas. Una anidación de demasiadas funciones puede tener el efecto opuesto, haciendo que el código sea menos fácil de mantener.

En un lenguaje basado en pilas , la composición funcional es aún más natural: se realiza mediante concatenación y suele ser el método principal de diseño de programas. El ejemplo anterior en Forth :

novia

Lo que tomará lo que haya en la pila antes, aplicará g, luego f y dejará el resultado en la pila. Consulte la notación de composición de sufijos para conocer la notación matemática correspondiente.

Nombrar la composición de funciones

Ahora supongamos que la combinación de llamar a f() sobre el resultado de g() es frecuentemente útil, y que queremos llamar foo() para que se use como una función por derecho propio.

En la mayoría de los lenguajes, podemos definir una nueva función implementada por composición. Ejemplo en C :

flotar foo ( flotar x ) { devolver f ( g ( x )); }     

(la forma larga con intermedios también funcionaría). Ejemplo en Forth :

  :foo novia;

En lenguajes como C , la única forma de crear una nueva función es definirla en el código fuente del programa, lo que significa que las funciones no se pueden componer en tiempo de ejecución . Sin embargo, es posible evaluar una composición arbitraria de funciones predefinidas :

#incluir <stdio.h> tipo definido int FXN ( int );  int f ( int x ) { devuelve x + 1 ; } int g ( int x ) { devuelve x * 2 ; } int h ( int x ) { devuelve x -3 ; }                  int eval ( FXN * fs [], int tamaño , int x ) { para ( int i = 0 ; i < tamaño ; i ++ ) x = ( * fs [ i ])( x );               devolver 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 )); }          

Composición de primera clase

En los lenguajes de programación funcional, la composición de funciones se puede expresar de forma natural como una función o un operador de orden superior . En otros lenguajes de programación, puedes escribir tus propios mecanismos para realizar la composición de funciones.

Haskell

En Haskell , el ejemplo foo = f  ∘   g dado anteriormente se convierte en:

foo = f . g

utilizando el operador de composición incorporado (.) que puede leerse como f después de g o g compuesto con f .

El operador de composición  ∘   se puede definir en Haskell usando una expresión lambda :

( . ) :: ( b -> c ) -> ( a -> b ) -> a -> c f . g = \ x -> f ( g x )                    

La primera línea describe el tipo de (.): toma un par de funciones, f ,   g, y devuelve una función (la expresión lambda en la segunda línea). Tenga en cuenta que Haskell no requiere la especificación de los tipos exactos de entrada y salida de f y g; a, b, c y x son marcadores de posición; solo importa la relación entre f y   g (f debe aceptar lo que devuelve g). Esto hace que (.) sea un operador polimórfico .

Ceceo

Las variantes de Lisp , especialmente Scheme , la intercambiabilidad de código y datos junto con el tratamiento de funciones se prestan extremadamente bien para una definición recursiva de un operador compositivo variádico .

( define ( compose . fs ) ( if ( null? fs ) ( lambda ( x ) x ) ; si no se proporciona ningún argumento, se evalúa como la función identidad ( lambda ( x ) (( car fs ) (( apply compose ( cdr fs )) x )))))                   ; ejemplos ( define ( add-a-bang str ) ( string-append str "!" ))     ( definir givebang ( componer cadena->símbolo agregar-un-bang símbolo->cadena ))     ( givebang 'set ) ; ===> ¡conjunto!  ; composición anónima (( componer raíz cuadrada negar cuadrado ) 5 ) ; ===> 0+5i     

APL

Muchos dialectos de APL incorporan la composición de funciones mediante el símbolo . Esta función de orden superior extiende la composición de funciones a la aplicación diádica de la función del lado izquierdo, de modo que A f∘g Bsea A f g B.

foo f g

Además, puedes definir la composición de funciones:

o { ⍺⍺ ⍵⍵ }  

En los dialectos que no admiten la definición en línea mediante llaves, está disponible la definición tradicional:

r ( f o g ) x r f g x       

Raku

Raku , al igual que Haskell , tiene un operador de composición de funciones incorporado, la principal diferencia es que se escribe como o o.

mi & foo = & f & g ;     

También, como en Haskell, puedes definir el operador tú mismo. De hecho, el siguiente es el código Raku utilizado para definirlo en la implementación de Rakudo .

# la implementación tiene una línea ligeramente diferente aquí porque hace trampa en proto sub infix :<∘> (&?, &?) is equiv(&[~]) is assoc<left> { * }  multi sub infix :<∘> () { *. self } # permite que `[∘] @array` funcione cuando `@array` está vacío multi sub infix :<∘> (&f) { & f } # permite que `[∘] @array` funcione cuando `@array` tiene un elemento multi sub infix :<∘> (&f, &g --> Block) { ( & f ) . count > 1 ?? -> | args { f | g | args } !! -> | args { f g | args } }                               # alias la ortografía "Texas" (todo es más grande y ASCII en Texas) mi & infijo: <o> : = & infijo: <∘> ;   

Nim

Nim admite una sintaxis de llamada de función uniforme , lo que permite la composición arbitraria de funciones a través del .operador de sintaxis de método. [3]

func foo ( a : int ) : cadena = $ a func bar ( a : cadena , conteo : int ) : seq [ cadena ] = para i en 0 .. < conteo : resultado.add ( a ) func baz ( a : seq [ cadena ] ) = para i en a : echo i                           # equivalente! echo foo ( 5 ) .bar ( 6 ).baz ( ) echo baz ( bar ( 6 , foo ( 5 )))   

Pitón

En Python , una forma de definir la composición de cualquier grupo de funciones es usando la función de reducción (use functools.reduce en Python 3):

# Disponible desde Python v2.6 desde  functools  import  reduce desde  writing  import  Callabledef  compose ( * funcs )  ->  Callable [[ int ],  int ]: """Componga un grupo de funciones (f(g(h(...)))) en una única función compuesta.""" return reduce ( lambda f , g : lambda x : f ( g ( x )), funcs )         # Ejemplo f  =  lambda  x :  x  +  1 g  =  lambda  x :  x  *  2 h  =  lambda  x :  x  -  3# Llamar a la función x=10 : ((x-3)*2)+1 = 15 print ( compose ( f ,  g ,  h )( 10 ))

JavaScript

En JavaScript podemos definirlo como una función que toma dos funciones f y g, y produce una función:

función o ( f , g ) { devolver función ( x ) { devolver f ( g ( x )); } }         // Alternativamente, utilizando el operador rest y expresiones lambda en ES2015 const compose = (... fs ) => ( x ) => fs . reduceRight (( acc , f ) => f ( acc ), x )           

DO#

En C# podemos definirlo como un método de extensión que toma las funciones f y g, y produce una nueva función:

// Ejemplo de llamada: // var c = f.ComposeWith(g); // // Func<int, bool> g = _ => ... // Func<bool, string> f = _ => ...público estático Func < T1 , T3 > ComposeWith < T1 , T2 , T3 > ( esta Func < T2 , T3 > f , Func < T1 , T2 > g ) => x => f ( g ( x ));                

Rubí

Lenguajes como Ruby te permiten construir tú mismo un operador binario:

clase Proc def componer ( otra_función ) -> ( * como ) { otra_función . llamar ( llamar ( * como )) } fin alias_metodo :+ , :componer fin           f = -> ( x ) { x * 2 } g = -> ( x ) { x ** 3 } ( f + g ) . llamar ( 12 ) # => 13824                 

Sin embargo, en Ruby 2.6 se introdujo un operador de composición de funciones nativo: [4]

f = proc { | x | x + 2 } g = proc { | x | x * 3 } ( f << g ) . call ( 3 ) # -> 11; idéntico a f(g(3)) ( f >> g ) . call ( 3 ) # -> 15; idéntico a g(f(3))                

Encuesta de investigación

Las nociones de composición, incluido el principio de composicionalidad y componibilidad , son tan omnipresentes que han surgido numerosas líneas de investigación independientes. A continuación se presenta una muestra del tipo de investigación en la que la noción de composición es central.

Composición a gran escala

Los programas o sistemas completos pueden ser tratados como funciones, que pueden ser compuestas fácilmente si sus entradas y salidas están bien definidas. [5] Las tuberías que permiten una fácil composición de filtros tuvieron tanto éxito que se convirtieron en un patrón de diseño de sistemas operativos.

Los procedimientos imperativos con efectos secundarios violan la transparencia referencial y, por lo tanto, no se pueden componer de forma limpia. Sin embargo, si se considera el "estado del mundo" antes y después de ejecutar el código como su entrada y salida, se obtiene una función limpia. La composición de tales funciones corresponde a ejecutar los procedimientos uno después del otro. El formalismo de mónadas utiliza esta idea para incorporar efectos secundarios y entrada/salida (E/S) en lenguajes funcionales.

Véase también

Notas

  1. ^ Cox (1986), págs. 15-17
  2. ^ DeMarco y Lister (1995), págs. 133-135.
  3. ^ "Manual de Nim: Sintaxis de llamada de método". nim-lang.org . Consultado el 17 de agosto de 2023 .
  4. ^ "Lanzamiento de Ruby 2.6.0". www.ruby-lang.org . Consultado el 4 de enero de 2019 .
  5. ^ Raymond (2003)

Referencias