Skip to main content

Fun with Lambdas: C++14 Style (part 1)

It's common knowledge that Functional Programming is spreading like a wildfire in mainstream languages. Latest promoted languages: Java 8 and C++, both of which now support lambdas. So, let the lambdas begin! and may the fun be ever on your side. The same text is available in slides form on Slideshare. This blog post and the talk/slides are inspired by JSON inventor Douglas Crockford.

Write an Identity function that takes an argument and returns the same argument.
auto Identity = [](auto x) {
  return x;
};
Identity(3); // 3
Write 3 functions add, sub, and mul that take 2 parameters each and return their sum, difference, and product respectively.
auto add = [](auto x, auto y) {
  return x + y;
}; 
auto sub = [](auto x, auto y) {
  return x - y;
};
int mul (int x, int y) {
  return x * y;
};

Write a function, identityf, that takes an argument and returns an inner class object that returns that argument.
auto identityf = [](auto x) {
  class Inner {
    int x;
    public: Inner(int i): x(i) {}
    int operator() () { return x; }
  };
  return Inner(x);
};
identityf(5)(); // 5

Write a function, identityf, that takes an argument and returns a function that returns that argument.
auto identityf = [](auto x) {
  return [=]() { return x; };
};
identityf(5)(); // 5

Lambda != Closure
  • A lambda is just a syntax sugar to define anonymous functions and function objects. 
  • A closure in C++ is a function object which closes over the environment in which it was created. The line #2 above defines a closure that closes over x.
  • A lambda is a syntactic construct (expression), and a closure is a run-time object, an instance of a closure type. 
  • C++ closures do not extend the lifetime of their context. (If you need this use shared_ptr)
Write a function that produces a function that returns values in a range.
auto fromto = [](auto start, auto finish) {    
  return [=]() mutable {      
    if(start < finish)        
      return start++;      
    else        
      throw std::runtime_error(“Complete");    
  };  
};
auto range = fromto(0, 10); 
range(); // 0
range(); // 1
Write a function that adds from two invocations.
auto addf = [](auto x) {
  return [=](auto y) { 
    return x+y; 
  };
};
addf(5)(4); // 9
Write a function swap that swaps the arguments of a binary function.
auto swap =[](auto binary) {
  return [=](auto x, auto y) {
    return binary(y, x);
  };
};
swap(sub)(3, 2); // -1
Write a function twice that takes a binary function and returns a unary function that passes its argument to the binary function twice.
auto twice =[](auto binary) {
  return [=](auto x) {
    return binary(x, x);
  };
};
twice(add)(11); // 22
Write a function that takes a binary function and makes it callable with two invocations.
auto applyf = [](auto binary) {
  return [=](auto x) { 
    return [=](auto y) {
      return binary(x, y); 
    };
  };
};
applyf(mul)(3)(4); // 12
Write a function that takes a function and an argument and returns a function that takes the second argument and applies the function.
auto curry = [](auto binary, auto x) {
  return [=](auto y) { 
    return binary(x, y);
  };
};
curry(mul, 3)(4); // 12
Currying (schönfinkeling)
  • Currying is the technique of transforming a function that takes multiple arguments in such a way that it can be called as a chain of functions, each with a single argument.
  • In lambda calculus functions take a single argument only.
  • Must know Currying to understand Haskell.
  • Currying != Partial function application
Partial Function Application
auto addFour = [](auto a, auto b, 
                  auto c, auto d) {
  return a+b+c+d;
};
auto partial = [](auto func, auto a, auto b) {
  return [=](auto c, auto d) { 
    return func(a, b, c, d);
  };
};
partial(addFour,1,2)(3,4); //10
Without creating a new function show 3 ways to create the inc function.
auto inc = curry(add, 1);
auto inc = addf(1);
auto inc = applyf(add)(1);
Write a function composeu that takes two unary functions and returns a unary function that calls them both.
auto composeu =[](auto f1, auto f2) {
  return [=](auto x) {
    return f2(f1(x));
  };
};
composeu(inc1, curry(mul, 5))(3) // 20
Write a function that returns a function that allows a binary function to be called exactly once.
auto once = [](auto binary) {    
  bool done = false;    
  return [=](auto x, auto y) mutable {
    if(!done) {        
      done = true;        
      return binary(x, y);      
    }      
    else        
      throw std::runtime_error("once!");     
  };  
};
once(add)(3,4); // 7
Write a function that takes a binary function and returns a function that takes two arguments and a callback.
auto binaryc = [](auto binary) {    
  return [=](auto x, auto y, auto callbk) {
   return callbk(binary(x,y));    
  };  
};
binaryc(mul)(5, 6, inc) // 31
binaryc(mul)(5,6,[](int a) { return a+1; });
Write 3 functions:
  • unit – same as Identityf
  • stringify – that stringifies its argument and applies unit to it
  • bind – that takes a result of unit and returns a function that takes a callback and returns the result of callback applied to the result of unit.
auto unit = [](auto x) { 
  return [=]() { return x; };
};
auto stringify = [](auto x) {    
  std::stringstream ss;
  ss << x;
  return unit(ss.str());
};

auto bind = [](auto u) {    
  return [=](auto callback) {
   return callback(u());    
  };  
}; 
Then verify.
std::cout << "Left Identity "  
          << stringify(15)() 
          << "==" 
          << bind(unit(15))(stringify)()
          << std::endl;

std::cout << "Right Identity " 
          << stringify(5)() 
          << "=="
          << bind(stringify(5))(unit)()
          << std::endl;
Why are unit and bind special? Read more about them here.

Comments

HiroshimaCC said…
Is the Right Identity supposed to be true?
Sumant Tambe said…
@HiroshimaCC: Good point. There was a oversight because "5" and 5 look the same when printed to stdout. The code now updated.
Jagan said…
I am unable to figure out how to get the values in range. I mean how to execute it to get the values in range ?

fromto(0, 10); would return the lambda and from(0, 10)(); will only print 0.
Isn't that the case ?
Jagan said…
This comment has been removed by the author.
Igor Mikushkin said…
Please correct identityf. It should use templated class according to description.
Sumant Tambe said…
@Jagan. fromto returns a range lambda that returns numbers from 0 to 9 one at a time. I've updated the code to reflect that.

@Igor: Are you referring to identityf::Inner? For simplicity, I'm using just an int member.
Stuart said…
@Sumant, I enjoyed this post, as I enjoy most of your posts. Thanks for sharing!
Michael said…
Interesting, thank you!

The Lambda != Closure section is confusing though.

"A lambda is just an anonymous function." -- I understand the desire to make the explanation as simple as possible, but here it happens at the cost of correctness. "Just functions" don't carry state. Perhaps "A lambda is just a syntax sugar to define anonymous functions and function objects"?

"Not all closures are lambdas and not all lambdas are closures." -- sounds weird. A lambda is a syntactic construct (expression), and a closure is a run-time object, an instance of a closure type. Neither can "be" the other.
Sumant Tambe said…
@Michael: I agree. The text was confusing and somewhat incorrect. I've taken the liberty to use your words in the updated post.
joaof said…
you can make the first identityf more like the identityf lamba:

auto identityf = [](auto x)
{
class inner
{
public:
using X = decltype(x);
X mx;
inner(X x): mx(x) {}
X operator()() { return mx; }
};
return inner{x};
};

(you version of inner uses int)
mjohn5 said…
Why are add and sub auto, but mult explicitly uses int?
Sumant Tambe said…
@mjohn5 No good reason! Just a reminder that good old functions are equally good candidates.
Dave Brown said…
Great read; thanks for posting!

Jumping on the back of @joaof's comment: Using decltype to ascertain the type of auto x allows for using an interesting pattern of, what the D Language community calls, Voldemort types. Walter Bright wrote an interesting article about it which you can read here.

Interesting to note that, unlike in D, we can later retrieve the type of Inner outside of the lambda:

auto foo = identifyf(5);
foo(); //5
using Foo = decltype(foo);
Foo bar(10);
bar(); //10
Sumant Tambe said…
@dave Aliasing the type of lambda is fine but are you sure about the rest? I get compiler error for "Foo bar(10)" and is_constructible returns false. Besides, there are many limitations like missing copy-assignment operator for lambdas. class Inner, however it copy-assignable.

Live code: http://melpon.org/wandbox/permlink/mfteteOhqXrZGxVS



Dave Brown said…
@Sumant

I am quite sure about the rest: http://ideone.com/h2ZRs3 :) I think, maybe, that I was confusing something in your example contra mine.

/Dave
Sumant Tambe said…
@Dave Oh yeah, when you have a named inner class inside a lambda, then what you are saying makes sense. However, if the lambda returns a closure then there are few limitations.
Dave Brown said…
Yup, I missed that, admittedly, not-so-subtle difference :)
black_13 said…
you stated: "C++ closures do not extend the lifetime of their context. (If you need this use shared_ptr)" could you give
and example?
mjohn5 said…
The point is that c++ lambdas are *not* closures because they just honor the external lifetime of the object without making sure the object survives as long as the lambda does. In c++ one way to lengthen an object's lifetime is to make it a shared pointer, which is reference counted.
manho valentine said…
you stated: "C++ closures do not extend the lifetime of their context. (If you need this use shared_ptr)" could you give
and example?
gclub

Popular posts from this blog

Multi-dimensional arrays in C++11

What new can be said about multi-dimensional arrays in C++? As it turns out, quite a bit! With the advent of C++11, we get new standard library class std::array. We also get new language features, such as template aliases and variadic templates. So I'll talk about interesting ways in which they come together.

It all started with a simple question of how to define a multi-dimensional std::array. It is a great example of deceptively simple things. Are the following the two arrays identical except that one is native and the other one is std::array?

int native[3][4];
std::array<std::array<int, 3>, 4> arr;

No! They are not. In fact, arr is more like an int[4][3]. Note the difference in the array subscripts. The native array is an array of 3 elements where every element is itself an array of 4 integers. 3 rows and 4 columns. If you want a std::array with the same layout, what you really need is:

std::array<std::array<int, 4>, 3> arr;

That's quite annoying for two r…

Understanding Fold Expressions

C++17 has an interesting new feature called fold expressions. Fold expressions offer a compact syntax to apply a binary operation to the elements of a parameter pack. Here’s an example. template <typename... Args> auto addall(Args... args) { return (... + args); } addall(1,2,3,4,5); // returns 15. This particular example is a unary left fold. It's equivalent to ((((1+2)+3)+4)+5). It reduces/folds the parameter pack of integers into a single integer by applying the binary operator successively. It's unary because it does not explicitly specify an init (a.k.a. identity) argument. So, let add it. template <typename... Args> auto addall(Args... args) { return (0 + ... + args); } addall(1,2,3,4,5); // returns 15. This version of addall is a binary left fold. The init argument is 0 and it's redundant (in this case). That's because this fold expression is equivalent to (((((0+1)+2)+3)+4)+5). Explicit identity elements will come in handy a little la…

Folding Monadic Functions

In the previous two blog posts (Understanding Fold Expressions and Folding Functions) we looked at the basic usage of C++17 fold expressions and how simple functions can be folded to create a composite one. We’ll continue our stride and see how "embellished" functions may be composed in fold expressions.

First, let me define what I mean by embellished functions. Instead of just returning a simple value, these functions are going to return a generic container of the desired value. The choice of container is very broad but not arbitrary. There are some constraints on the container and once you select a generic container, all functions must return values of the same container. Let's begin with std::vector.
// Hide the allocator template argument of std::vector. // It causes problems and is irrelevant here. template <class T> struct Vector : std::vector<T> {}; struct Continent { }; struct Country { }; struct State { }; struct City { }; auto get_countries…