¿Es posible tener funciones de curry y variad al mismo tiempo?

13

Estoy pensando en hacer que las funciones de curry y variadic estén disponibles en un lenguaje de programación funcional de tipo dinámico, pero me pregunto si es posible o no.

Aquí hay algunos pseudocódigo:

sum = if @args.empty then 0 else @args.head + sum @args.tail

que supuestamente es para sumar todos sus argumentos. Luego, si sum se trata como un número, entonces el resultado es 0 . por ejemplo,

sum + 1

es igual a 1, suponiendo que + solo puede funcionar con números. Sin embargo, incluso sum == 0 es verdadero, sum todavía mantendrá su valor y propiedad funcional sin importar cuántos argumentos se den (por lo tanto, "parcialmente aplicado" y "variadic" al mismo tiempo), por ejemplo, si declaro

g = sum 1 2 3

entonces g es igual a 6 , sin embargo, podemos aplicar aún más g . Por ejemplo, g 4 5 == 15 es verdadero. En este caso, no podemos reemplazar el objeto g por un 6 literal, porque aunque producen el mismo valor cuando se tratan como un entero, contienen códigos diferentes dentro.

Si este diseño se usa en un lenguaje de programación real, ¿causará confusión o ambigüedad?

    
pregunta Michael Tsang 09.06.2015 - 10:13

3 respuestas

6

¿Cómo se pueden implementar varargs? Necesitamos algún mecanismo para señalar el final de la lista de argumentos. Esto puede ser

  • un valor de terminación especial, o
  • la longitud de la lista vararg pasada como un parámetro adicional.

Ambos de estos mecanismos se pueden usar en el contexto del curry para implementar varargs, pero la tipificación adecuada se convierte en un problema importante. Supongamos que estamos tratando con una función sum: ...int -> int , excepto que esta función usa currying (por lo que en realidad tenemos un tipo más parecido a sum: int -> ... -> int -> int , excepto que no conocemos el número de argumentos).

Caso: valor de terminador: Deje que end sea el terminador especial, y T sea el tipo de sum . Ahora sabemos que aplicando a end la función devuelve: sum: end -> int , y que aplicada a un int obtenemos otra función de suma: sum: int -> T . Por lo tanto, T es la unión de estos tipos: T = (end -> int) | (int -> T) . Al sustituir T , obtenemos varios tipos posibles, como end -> int , int -> end -> int , int -> int -> end -> int , etc. Sin embargo, la mayoría de los sistemas de tipos no admiten este tipo de tipos.

Caso: longitud explícita: el primer argumento de una función vararg es el número de varargs. Por lo tanto, sum 0 : int , sum 1 : int -> int , sum 3 : int -> int -> int -> int , etc. Esto es compatible con algunos sistemas de tipo y es un ejemplo de tipificación dependiente . En realidad, el número de argumentos sería un parámetro de tipo y no un parámetro regular. No tendría sentido que la aridad de la función dependiera de un valor de tiempo de ejecución, s = ((sum (floor (rand 3))) 1) 2 obviamente no está bien tipeado: esto se evalúa como s = ((sum 0) 1) 2 = (0 1) 2 , s = ((sum 1) 1) 2 = 1 2 o s = ((sum 2) 1) 2 = 3 .

En la práctica, ninguna de estas técnicas debe usarse, ya que son propensas a errores y no tienen un tipo (significativo) en los sistemas de tipos comunes. En su lugar, simplemente pase una lista de valores como un parámetro: sum: [int] -> int .

Sí, es posible que un objeto aparezca como una función y un valor, por ejemplo. En un sistema de tipo con coerciones. Deje que sum sea SumObj , que tiene dos coerciones:

  • coerce: SumObj -> int -> SumObj permite que sum se use como una función, y
  • coerce: SumObj -> int nos permite extraer el resultado.

Técnicamente, esta es una variación del valor del terminador en el caso anterior, con T = SumObj y coerce como un-envoltorio para el tipo. En muchos lenguajes orientados a objetos, esto es trivialmente implementable con sobrecarga de operadores, por ejemplo. C ++:

#include <iostream>
using namespace std;

class sum {
  int value;
public:
  explicit sum() : sum(0) {}
  explicit sum(int x) : value(x) {}
  sum operator()(int x) const { return sum(value + x); }  // function call overload
  operator int() const { return value; } // integer cast overload
};

int main() {
  int zero = sum();
  cout << "zero sum as int: " << zero << '\n';
  int someSum = sum(1)(2)(4);
  cout << "some sum as int: " << someSum << '\n';
}
    
respondido por el amon 09.06.2015 - 11:58
2

Es posible que desee ver esta implementación de printf en Haskell , junto con esta descripción de cómo funciona . Hay un enlace en la última página del documento de Oleg Kiselyov sobre cómo hacer este tipo de cosas, que también vale la pena leer. De hecho, si está diseñando un lenguaje funcional, el sitio web de Oleg probablemente debería ser una lectura obligatoria.

En mi opinión, estos enfoques son un poco pirateados, pero muestran que es posible. Sin embargo, si su idioma incluye la escritura totalmente dependiente, es mucho más simple. Una función variad para sumar sus argumentos enteros podría verse así:

type SumType = (t : union{Int,Null}) -> {SumType, if t is Int|
                                         Int,     if t is Null}
sum :: SumType
sum (v : Int) = v + sum
sum (v : Null) = 0

Una abstracción para definir el tipo recursivo sin necesidad de darle un nombre explícito podría facilitar la escritura de tales funciones.

Editar: por supuesto, acabo de leer la pregunta y dijiste un lenguaje tipificado dinámicamente , en cuyo punto, obviamente, la mecánica de tipos no es realmente relevante, y por lo tanto, la respuesta de @amon probablemente contiene todo necesitas. Oh, bueno, dejaré esto aquí en caso de que alguien lo descubra mientras se pregunta cómo hacerlo en un lenguaje estático ...

    
respondido por el Jules 15.08.2016 - 10:04
0

Aquí hay una versión para el análisis de funciones variadas en Python3 que utiliza el enfoque de "terminador" de @amon, aprovechando los argumentos opcionales de Python:

def curry_vargs(g):
    actual_args = []
    def f(a, force=False):
        nonlocal actual_args
        actual_args.append(a)
        if force:
            res = g(*actual_args)
            actual_args = []
            return res
        else:
            return f
    return f

def g(*args): return sum(args)
f = curry_vargs(g)
f(1)(2)(3)(4,True) # => 10

La función devuelta f recopila los argumentos que se le pasan en llamadas sucesivas en una matriz que está vinculada en el ámbito externo. Solo cuando el argumento force es verdadero, se llama a la función original con todos los argumentos recopilados hasta el momento.

Las advertencias de esta implementación son que siempre tienes que pasar un primer argumento a f para que no puedas crear un "procesador", una función donde todos los argumentos están vinculados y solo se puede llamar con la lista de argumentos vacía (pero creo que esto está en línea con la implementación típica de curry).

Otra advertencia es que una vez que pasas un argumento incorrecto (por ejemplo, del tipo incorrecto) tienes que volver a escribir la función original. No hay otra manera de restablecer la matriz interna, esto solo se hace después de una ejecución exitosa de la función de currículum.

No sé si su pregunta simplificada, "¿puede un objeto ser una función y un valor que no sea de función al mismo tiempo?", se puede implementar en Python, como una referencia a una función sin paréntesis se evalúa al Objeto de función interna. No sé si esto se puede doblar para devolver un valor arbitrario.

Probablemente sería fácil en Lisp, ya que los símbolos Lisp pueden tener un valor y un valor de función al mismo tiempo; el valor de la función se selecciona simplemente cuando el símbolo aparece en la posición de la función (como el primer elemento en una lista).

    
respondido por el ThomasH 14.08.2016 - 20:20

Lea otras preguntas en las etiquetas