¿Es posible tener una función curry y variada al mismo tiempo?

13

Estoy pensando en hacer que las funciones 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 suma todos sus argumentos. Entonces, si sumse trata a sí mismo un número, entonces el resultado es 0. por ejemplo,

sum + 1

es igual a 1, asumiendo que +solo puede funcionar en números. Sin embargo, incluso sum == 0es cierto, sumseguirá manteniendo 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 ges igual a 6, sin embargo, aún podemos aplicar más g. Por ejemplo, g 4 5 == 15es cierto. En este caso, no podemos reemplazar el objeto gpor un literal 6, porque aunque producen el mismo valor cuando se tratan como un entero, contienen diferentes códigos en su interior.

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

Michael Tsang
fuente
1
Hablando estrictamente, el uso del curry como fundamento de un lenguaje significa que todas las funciones son unarias, no solo no hay funciones variables, ¡ni siquiera hay funciones binarias! Sin embargo, los programas en ese lenguaje aún se verán como si tomaran múltiples argumentos, y eso se aplica tanto a las funciones variadas como a las normales.
Kilian Foth
Entonces mi pregunta se simplifica a "¿puede un objeto ser una función y un valor no funcional al mismo tiempo?" En el ejemplo anterior, no sumtiene 0argumento y recursivamente se llama a sí mismo con un argumento.
Michael Tsang
¿No es ese el trabajo de reduce?
Ratchet Freak
1
Echar un vistazo a las funciones que se está usando en args: empty, head, y tail. Esas son todas funciones de lista, lo que sugiere que quizás lo más fácil y más sencillo sería usar una lista donde estarían las cosas variadas. (Entonces, en sum [1, 2, 3]lugar de sum 1 2 3)
Michael Shaw

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 terminador especial, o
  • La longitud de la lista vararg pasada como parámetro adicional.

Ambos 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 utiliza curry (por lo que en realidad tenemos un tipo más parecido sum: int -> ... -> int -> int, excepto que no sabemos el número de argumentos).

Caso: valor del terminador: Sea endel terminador especial y Tel tipo de sum. Ahora sabemos que se aplica a endla función devuelve: sum: end -> inty que se aplica a un int tenemos otra suma similar a la función: sum: int -> T. Por lo tanto, Tes la unión de estos tipos: T = (end -> int) | (int -> T). Mediante la sustitución T, obtenemos diferentes tipos posibles, tales como end -> int, int -> end -> int, int -> int -> end -> int, etc. Sin embargo, la mayoría de los sistemas de tipo no dan cabida a esos tipos.

Caso: longitud explícita: el primer argumento para una función vararg es el número de varargs. Así sum 0 : int, sum 1 : int -> int, sum 3 : int -> int -> int -> intetc. Esto se apoya en 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 normal - no tendría sentido que la aridad de la función que depender de un valor de tiempo de ejecución, s = ((sum (floor (rand 3))) 1) 2es obviamente mal escrito-: Evalúa a cualquiera s = ((sum 0) 1) 2 = (0 1) 2, s = ((sum 1) 1) 2 = 1 2o 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, sólo tiene que pasar una lista de valores como uno 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 tipos con coacciones. Let sumbe a SumObj, que tiene dos coacciones:

  • coerce: SumObj -> int -> SumObjpermite sumser utilizado como una función, y
  • coerce: SumObj -> int nos permite extraer el resultado.

Técnicamente, esta es una variación del caso del valor del terminador anterior, con T = SumObj, y coercees un desempaquetador para el tipo. En muchos lenguajes orientados a objetos, esto es trivialmente implementable con sobrecarga del operador, 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';
}
amon
fuente
Respuesta impresionante! El inconveniente de empacar varargs en una lista es que pierde la aplicación parcial de curry. Estaba jugando con una versión de Python de su enfoque de terminador, usando un argumento de palabra clave ..., force=False)para forzar la aplicación de la función inicial.
ThomasH
Podría hacer su propia función de orden superior que aplique parcialmente una función que tome una lista, como curryList : ([a] -> b) -> [a] -> [a] -> b, curryList f xs ys = f (xs ++ ys).
Jack
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 al documento de Oleg Kiselyov sobre 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 de lectura obligatoria.

En mi opinión, estos enfoques son un poco piratas, pero muestran que es posible. Sin embargo, si su idioma presenta una escritura totalmente dependiente, es mucho más simple. Una función variada 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 nuevamente y dijiste un lenguaje escrito dinámicamente , en ese punto obviamente la mecánica de tipo no es realmente relevante, y por lo tanto la respuesta de @ amon probablemente contiene todo lo que necesitas. Oh, bueno, dejaré esto aquí en caso de que alguien se encuentre con esto mientras se pregunta cómo hacerlo en un lenguaje estático ...

Jules
fuente
0

Aquí hay una versión para curry de funciones variadas en Python3 que utiliza el enfoque "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 frecopila argumentos pasados ​​en llamadas sucesivas en una matriz que está vinculada en el ámbito externo. Solo cuando el forceargumento es verdadero, se llama a la función original con todos los argumentos recopilados hasta ahora.

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

Otra advertencia es que una vez que pasa un argumento incorrecto (por ejemplo, del tipo incorrecto), debe volver a curry la función original. No hay otra forma de restablecer la matriz interna, esto solo se hace después de una ejecución exitosa de la función currificada.

No sé si su pregunta simplificada, "¿puede un objeto ser una función y un valor no funcional al mismo tiempo?", Puede implementarse en Python, ya que una referencia a una función sin paréntesis se evalúa como el objeto interno de la función . No sé si esto puede doblarse para devolver un valor arbitrario.

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

ThomasH
fuente