Composición de funciones (informática) - Function composition (computer science)

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 todo.

Los programadores aplican con frecuencia 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 es interesante como una función por derecho propio, para ser utilizada posteriormente. Una función de este tipo siempre se puede definir, pero los lenguajes con funciones de primera clase lo hacen más fácil.

La capacidad de componer funciones fácilmente fomenta la factorización (separación) de funciones para la mantenibilidad y la reutilización del código . De manera más general, los grandes sistemas pueden construirse componiendo programas completos.

Hablando en términos estrictos, la composición de funciones se aplica a funciones que operan en una cantidad finita de datos, cada paso lo procesa secuencialmente antes de pasarlo al siguiente. Las funciones que operan en datos potencialmente infinitos (una secuencia u otros datos codatos ) se conocen como filtros y, en cambio, se conectan en una tubería , que es análoga a la composición de funciones y se puede ejecutar simultáneamente .

Componer llamadas a funciones

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

float 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 solo una línea de código y se la conoce coloquialmente como una forma "altamente compuesta". La legibilidad y, por tanto, la facilidad de mantenimiento es una ventaja de los formularios altamente compuestos, ya que requieren menos líneas de código, minimizando el "área de superficie" de un programa. DeMarco y Lister verifican empíricamente una relación inversa entre área de superficie y mantenibilidad. Por otro lado, puede ser posible abusar de formas muy compuestas. Un anidamiento de demasiadas funciones puede tener el efecto contrario, haciendo que el código sea menos fácil de mantener.

En un lenguaje basado en pilas , la composición funcional es incluso 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 :

g f

Que tomará lo que estaba en la pila antes, aplica g, luego f, y deja el resultado en la pila. Consulte la notación de composición de sufijo para obtener la notación matemática correspondiente.

Nombrar la composición de funciones

Ahora suponga que la combinación de llamar a f () sobre el resultado de g () es frecuentemente útil, y que queremos nombrar foo () para usarla 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 :

float foo(float x) {
    return f(g(x));
}

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

  : foo g f ;

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

#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));
}

Composición de primera

En los lenguajes de programación funcional, la composición de funciones se puede expresar naturalmente como una función u operador de orden superior . En otros lenguajes de programación, puede escribir sus propios mecanismos para realizar la composición de funciones.

Haskell

En Haskell , el ejemplo anterior se convierte en:

foo = f . g

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

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

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

Las primeras líneas describen el tipo de (.): Toma un par de funciones y devuelve una función. Tenga en cuenta que Haskell no requiere la especificación de los tipos exactos de entrada y salida de f y g, solo las relaciones entre ellos (f debe aceptar lo que devuelve g). Esto convierte a (.) En un operador polimórfico .

Ceceo

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 composicional variable .

(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 negate square) 5) ; ===> 0+5i

APL

Muchos dialectos de APL incorporan una función de composición que utiliza 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 tal manera que A f∘g B sea A f g B .

foofg

Además, puede definir la composición de la función:

o{⍺⍺ ⍵⍵ }

En el dialecto que no admite la definición en línea con llaves, la definición tradicional está disponible:

 r(f o g)x
  rf g x

Raku

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

my &foo = &f  &g;

También, como Haskell , podría definir el operador usted mismo. De hecho, el siguiente es el código Raku utilizado para definirlo en la implementación de Rakudo .

# the implementation has a slightly different line here because it cheats
proto sub infix:<∘> (&?, &?) is equiv(&[~]) is assoc<left> {*}

multi sub infix:<∘> () { *.self } # allows `[∘] @array` to work when `@array` is empty
multi sub infix:<∘> (&f) { &f }   # allows `[∘] @array` to work when `@array` has one element
multi sub infix:<∘> (&f, &g --> Block) {
    (&f).count > 1
    ?? -> |args { f |g |args }
    !! -> |args { f g |args }
}

# alias it to the "Texas" spelling ( everything is bigger, and ASCII in Texas )
my &infix:<o> := &infix:<∘>;

Pitón

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

# Available since Python v2.6
from functools import reduce

def compose(*funcs) -> 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))

JavaScript

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

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)

C#

En C # podemos definirlo como un Func que toma dos Funcs f y g, y produce un Func:

// Call example:
//   var c = Compose(f, g);
//
//   Func<int, bool> g = _ =>  ...
//   Func<bool, string> f = _ => ...

Func<TIn, TOut> Compose<TIn, TMid, TOut>(Func<TMid, TOut> f, Func<TIn, TMid> g) => _ => f(g(_));

Rubí

Los lenguajes como Ruby le permiten construir un operador binario usted mismo:

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

Sin embargo, se introdujo un operador de composición de función nativa en 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))

Encuesta de investigación

Las nociones de composición, incluido el principio de composicionalidad y capacidad de composición , son tan omnipresentes que numerosas líneas de investigación han evolucionado por separado. La siguiente es 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 tratarse como funciones, que se pueden componer fácilmente si sus entradas y salidas son conductos bien definidos que permiten una composición fácil de filtros y 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 limpiamente. Sin embargo, si considera el "estado del mundo" antes y después de ejecutar el código como entrada y salida, obtendrá una función limpia. La composición de tales funciones corresponde a ejecutar los procedimientos uno tras otro. El formalismo de las mónadas utiliza esta idea para incorporar efectos secundarios y E / S en lenguajes funcionales.

Ver también

Notas

Referencias