¿Hay un nombre para este modismo de creación de tuplas?


Sobre el Aumentar la lista de correo, el siguiente truco inteligente para crear una entidad similar a tupla fue publicado recientemente por @LouisDionne:

#include <iostream>

auto list = [](auto ...xs) { 
    return [=](auto access) { return access(xs...); }; 
}; 

auto length = [](auto xs) { 
    return xs([](auto ...z) { return sizeof...(z); }); 
};

int main()
{
    std::cout << length(list(1, '2', "3")); // 3    
}

Ejemplo En Vivo.

La inteligencia es que list es una lambda que toma una lista de parámetros variádicos como entrada, y devuelve una lambda como salida que tomará otra lambda para actuar sobre su entrada. Del mismo modo, length es una lambda que toma una entidad tipo lista, a la que suministrará el operador variadic sizeof... al parámetros de entrada originales de la lista. El operador sizeof... está envuelto dentro de una lambda para que se pueda pasar al list.

Pregunta : ¿hay un nombre para este modismo de creación de tuplas? Tal vez de un lenguaje de programación funcional donde las funciones de orden superior se utilizan más comúnmente.

Author: TemplateRex, 2014-08-16

3 answers

Creo que esta es una implementación sutil de una cosa similar a la mónada, específicamente algo en el mismo espíritu de la mónada de continuación.

Las mónadas son una construcción de programación funcional utilizada para simular el estado entre diferentes pasos de un cálculo (Recuerde que un lenguaje funcional es apátrida).
Lo que hace una mónada es encadenar diferentes funciones, creando una "tubería de cálculo" donde cada paso conoce el estado actual de la calculo.

Las mónadas tienen dos pilares primarios:

  • Una función return, que toma un valor y lo devuelve en una forma lista para mónadas.
  • Una función bind, que toma un valor listo para la Mónada (del paso anterior de la canalización) y lo desenvuelve a su original desde para pasar el valor al siguiente paso.

La Wikipedia tiene muy buenos ejemplos y explicaciones sobre las mónadas.

Permítanme reescribir el código dado de C++14:

auto list = []( auto... xs ) 
{ 
    return [=]( auto access ) { return access(xs...); };
};

Creo que aquí identificar la función return de una mónada: Toma el valor y lo devuelve de forma Monádica. Específicamente, este retorno devuelve un funtor (En el sentido matemático, no un funtor de C++) que va de la categoría "tupla" a la categoría variadic pack.

auto pack_size = [](auto... xs ) { return sizeof...(xs); };

pack_size es sólo una función normal. Se usaría en una tubería para hacer algún trabajo.

auto bind = []( auto xs , auto op ) 
{
    return xs(op);
};

Y length es solo una versión no genérica de algo cercano al operador monada bind, un operador que toma un valor monádico de un paso de canalización anterior, y lo pasa a la función especificada (Función que realmente hace el trabajo). Esa función es la funcionalidad realizada por este paso de cálculo.

Finalmente tu llamada podría ser reescrita como:

auto result = bind(list(1,'2',"3"), pack_size);

So, ¿cuál es el nombre de este modismo de creación de tuplas? Bueno, creo que esto podría llamarse" monad-like tuplas", ya que no es exactamente una mónada, pero la representación y expansión de la tupla funciona de manera similar, quedando a la Haskell continuación mónada.

Editar: Más divertido

Solo para sacudir la divertida programación en C++, he seguido explorando esta cosa similar a la mónada. Puedes encontrar algunos ejemplos aquí.

 31
Author: Manu343726,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-23 20:09:26

Yo llamaría a este modismo tupla-continuador o, más en general, monádico-continuador. Es definitivamente un ejemplo de una mónada de continuación. Una gran introducción para la continuación monad para programadores de C++ es aquí. En esencia, la lambda list anterior toma un valor (un paquete de parámetros variádicos) y devuelve un simple 'continuador' (el cierre interno). Este continuador, cuando se le da un callable (called access), pasa el paquete de parámetros en él y devuelve lo que sea que devuelva ese callable.

Tomando prestado del blogpost de FPComplete, un continuador es más o menos como el siguiente.

template<class R, class A>
struct Continuator {
    virtual ~Continuator() {}
    virtual R andThen(function<R(A)> access) = 0;
};

El Continuator anterior es abstracto not no proporciona una implementación. Por lo tanto, aquí es una simple.

template<class R, class A>
struct SimpleContinuator : Continuator<R, A> {
    SimpleContinuator (A x) : _x(x) {}
    R andThen(function<R(A)> access) {
        return access(_x);
    }
    A _x;
};

El SimpleContinuator acepta un valor de tipo A y lo pasa a access cuando se llama andThen. El list lambda anterior es esencialmente el mismo. Es más general. En lugar de un solo valor, el cierre interior captura un parameter-pack y lo pasa a la función access. ¡Genial!

Esperemos que eso explique lo que significa ser un continuador. pero, ¿qué significa ser una mónada? Aquí hay una buena introducción usando imágenes.

Creo que la list lambda es también una mónada de lista, que se implementa como una mónada de continuación. Nótese que la mónada de continuación es la madre de todas las mónadas. Es decir, puede implementar cualquier mónada con una mónada de continuación. Por supuesto, monada lista no está fuera de llegar.

Como un paquete de parámetros es naturalmente una 'lista' (a menudo de tipos heterogéneos), tiene sentido que funcione como una mónada de lista/secuencia. La lambda list anterior es una forma muy interesante de convertir paquetes de parámetros de C++ a una estructura monádica. Por lo tanto, las operaciones se pueden encadenar una tras otra.

La length lambda anterior, sin embargo, es un poco decepcionante porque rompe la mónada y la lambda anidada dentro simplemente devuelve un entero. Podría decirse que hay un mejor manera de escribir la longitud 'getter' como se muestra a continuación.

----Functor----

Antes de que podamos decir que la lista lambda es una mónada, tenemos que demostrar que es un funtor. Es decir, fmap debe ser escrito para la lista.

La lista lambda anterior sirve como el creador del funtor de un paquete de parámetros---esencialmente sirve como el return. Ese funtor creado mantiene el paquete de parámetros consigo mismo (captura) y permite 'acceder' a él siempre que le des un acepta un número variable de argumentos. Tenga en cuenta que el llamable se llama EXACTAMENTE-UNA VEZ.

Permite escribir fmap para tal funtor.

auto fmap = [](auto func) { 
    return [=](auto ...z) { return list(func(z)...); };
};

El tipo del func debe ser (a -> b). Es decir, en C++ hablar,

template <class a, class b>
b func(a);

El tipo de fmap es fmap: (a -> b) -> list[a] -> list[b] Es decir, en lenguaje C++,

template <class a, class b, class Func>
list<b> fmap(Func, list<a>);

Es decir, fmap simplemente mapea la lista-de-a a una lista-de-b.

Ahora puedes hacer{[53]]}

auto twice = [](auto i) { return 2*i; };
auto print = [](auto i) { std::cout << i << " "; return i;};
list(1, 2, 3, 4)
    (fmap(twice))
    (fmap(print)); // prints 2 4 6 8 on clang (g++ in reverse)

Por lo tanto, es un funtor.

----Mónada----

Ahora, intentemos para escribir un flatmap (a. k. a. bind, selectmany)

El tipo de mapa plano es flatmap: (a -> list[b]) -> list[a] -> list[b].

Es decir, dada una función que mapea a a una lista-de-b y una lista-de-a, flatmap devuelve una lista-de-b. Esencialmente, toma cada elemento de la lista-de-a, llama a func sobre él, recibe una lista-de-b (potencialmente vacía) una por una, luego concatena toda la lista-de-b, y finalmente devuelve la lista-de-b final.

Aquí hay una implementación de flatmap para list.

auto concat = [](auto l1, auto l2) {
    auto access1 = [=](auto... p) {
      auto access2 = [=](auto... q) {
        return list(p..., q...);
      };
      return l2(access2);
    };
    return l1(access1);
};

template <class Func>
auto flatten(Func)
{
  return list(); 
}

template <class Func, class A>
auto flatten(Func f, A a)
{
  return f(a); 
}

template <class Func, class A, class... B>
auto flatten(Func f, A a, B... b)
{
  return concat(f(a), flatten(f, b...));
}

auto flatmap = [](auto func) {
  return [func](auto... a) { return flatten(func, a...); };
};

Ahora puedes hacer un montón de cosas poderosas con una lista. Por ejemplo,

auto pair  = [](auto i) { return list(-i, i); };
auto count = [](auto... a) { return list(sizeof...(a)); };
list(10, 20, 30)
    (flatmap(pair))
    (count)
    (fmap(print)); // prints 6.

La función count es una operación monad-perserving porque devuelve una lista de un solo elemento. Si realmente desea obtener la longitud (no envuelta en una lista), debe terminar la cadena monádica y obtener el valor de la siguiente manera.

auto len = [](auto ...z) { return sizeof...(z); }; 
std::cout << list(10, 20, 30)
                 (flatmap(pair))
                 (len);

Si se hace correctamente, el patrón de canalización de colección (p. ej., filter, reduce) ahora se puede aplicar a paquetes de parámetros de C++. ¡Órale!

----Mónada Leyes----

Asegurémonos de que la list mónada satisface las tres leyes de la mónada.

auto to_vector = [](auto... a) { return std::vector<int> { a... }; };

auto M = list(11);
std::cout << "Monad law (left identity)\n";
assert(M(flatmap(pair))(to_vector) == pair(11)(to_vector));

std::cout << "Monad law (right identity)\n";
assert(M(flatmap(list))(to_vector) == M(to_vector));

std::cout << "Monad law (associativity)\n";
assert(M(flatmap(pair))(flatmap(pair))(to_vector) == 
       M(flatmap([=](auto x) { return pair(x)(flatmap(pair)); }))(to_vector));

Todas las afirmaciones están satisfechas.

----Canalización de Recogida----

Aunque la anterior 'lista' lambda es probablemente una mónada y comparte características de la proverbial 'lista-mónada', es bastante desagradable. Especialmente, porque el comportamiento de los combinadores comunes collection pipeline, como filter (también conocido como where) no cumple con common expectativas.

La razón es cómo funcionan las lambdas de C++. Cada expresión lambda produce un objeto de función de un tipo único. Por lo tanto, list(1,2,3) produce un tipo que no tiene nada que ver con list(1) y una lista vacía, que en este caso sería list().

La implementación directa de where falla en la compilación porque en C++ una función no puede devolver dos tipos diferentes.

auto where_broken = [](auto func) {
  return flatmap([func](auto i) { 
      return func(i)? list(i) : list(); // broken :-(
  }); 
};

En la implementación anterior, func devuelve un booleano. Es un predicado que dice verdadero o falso para cada elemento. El ?: operator no compila.

Por lo tanto, se puede usar un truco diferente para permitir la continuación de la canalización de la colección. En lugar de filtrar realmente los elementos, simplemente se marcan como tales - - - y eso es lo que lo hace desagradable.

auto where_unpleasant = [](auto func) {
  return [=](auto... i) { 
      return list(std::make_pair(func(i), i)...);
  }; 
};

El {} [42]} hace el trabajo, pero desagradablemente...

Por ejemplo, así es como puede filtrar elementos negativos.

auto positive = [](auto i) { return i >= 0; };
auto pair_print = [](auto pair) { 
  if(pair.first) 
     std::cout << pair.second << " "; 
  return pair; 
};
list(10, 20)
    (flatmap(pair))
    (where_unpleasant(positive))
    (fmap(pair_print)); // prints 10 and 20 in some order

----Heterogéneo Tuplas----

Hasta ahora la discusión era sobre tuplas homogéneas. Ahora vamos a generalizarlo a tuplas verdaderas. Sin embargo, fmap, flatmap, where acepta sólo una llamada Lambda. Para proporcionar múltiples lambdas cada uno trabajando en un tipo, podemos sobrecargarlos. Por ejemplo,

template <class A, class... B>
struct overload : overload<A>, overload<B...> {
  overload(A a, B... b) 
      : overload<A>(a), overload<B...>(b...) 
  {}  
  using overload<A>::operator ();
  using overload<B...>::operator ();
};

template <class A>
struct overload<A> : A{
  overload(A a) 
      : A(a) {} 
  using A::operator();
};

template <class... F>
auto make_overload(F... f) {
  return overload<F...>(f...);   
}

auto test = 
   make_overload([](int i) { std::cout << "int = " << i << std::endl; },
                 [](double d) { std::cout << "double = " << d << std::endl; });
test(10); // int 
test(9.99); // double    

Usemos la técnica lambda sobrecargada para procesar un continuador de tupla heterogéneo.

auto int_or_string = 
        make_overload([](int i) { return 5*i; },
                      [](std::string s) { return s+s; });
    list(10, "20")
        (fmap(int_or_string))
        (fmap(print)); // prints 2020 and 50 in some order

Finalmente, Ejemplo En Vivo

 18
Author: Sumant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-29 17:10:13

Esto parece una forma de continuación pasando estilo.

La idea aproximada de CPS es la siguiente: en lugar de que una función (digamos f) devuelva algún valor, se le da a f otro argumento, que es una función, llamada continuación . Luego, f llama a esta continuación con el valor devuelto en lugar de devolver. Tomemos un ejemplo:

int f (int x) { return x + 42; }

Se convierte en

void f (int x, auto cont) { cont (x + 42); }

La llamada es una llamada de cola, y se puede optimizar en un salto (esta es la razón por la que TCO es obligatorio en algunos lenguajes, como Scheme, cuya semántica se basa en alguna forma de transformación en CPS).

Otro ejemplo:

void get_int (auto cont) { cont (10); }
void print_int (int x) { printf ("%d", x), }

Ahora puedes hacer get_int (std::bind (f, _1, print_int)) para imprimir 54. Tenga en cuenta que todas las llamadas de continuación son siempre llamadas de cola (la llamada a printf también es una llamada de continuación).

Un ejemplo bien conocido son las devoluciones de llamada asíncronas (llamadas AJAX en javascript, por ejemplo): se pasa una continuación a una rutina que se ejecuta en paralelo.

Las continuaciones pueden ser compuesto (y forma una mónada, en caso de que esté interesado), como en el ejemplo anterior. De hecho es posible transformar un programa (funcional) completamente en CPS, de modo que cada llamada es una llamada de cola (y entonces no necesita ninguna pila para ejecutar el programa !).

 3
Author: Alexandre C.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-02 20:20:15