Hey! This is great! I can fill my C++ code with statefull functors and lazy evaluation. The enemy shall not pass. No one will ever be able to debug it. Instant job security.
Seriously. I like experiments, but just keep this away from production code.
Sorry about being an asshole here, but really. The problem with articles like that, is that a novice developer, with some 5-10 years of experience can pick up an article similar to that one and use such techniques without any need. With disastrous consequences. How do I know it? I've seen it happen. Again and again. Hell, it had happened to myself, years back.
An article like that should start with: "unless you really really know what you are doing and are an adult (20+ years of coding) you should probably avoid using techniques like that in the production code."
Hah... "novice... with some 5-10 years of experience". Actually you're not too off the mark. And in any case, with c++11, pretty much everyone who's not been following the c++0x/c++1x process assiduously, counts as a novice.
Although having over 7 years experience as a c++ programmer, since I'm not using it so much now I haven't managed to keep up so much with the changes in c++11 and feel a bit noviced overnight.
Still, move constructors are pretty handy so that's one change I use whenever I can.
I'm pretty sure C++ (and any language for that matter) has more than enough quirky features to obfuscate any piece of code. No need to read this article for that. But if 20 years of experience makes lazy streams seem unfit for production, then someone has spent 10 years burying their head so deep in the sand that their feet are barely showing. Java's iterators, Python's generators, you work with them even in C. And is it not obvious that one needs to really know what they are doing if they are working as a professional developer?
The example of the article was simple of course. But solving the problem of printing Pythagorean triples was not the aim of the article. The aim was to show what makes lazy streams tick, to show that they can be implemented in C++ and that an uniform interface can be provided with functor, monoid and monad. Finally, the simple problem was solved with the new tool to show the inversion of control.
Don't get me wrong. I like functional approach. I am developing in functional languages as well as in C/C++. I think these techniques actually do have a place in the production level code.
But, the mail point of the post was, that if you write an article like that, it should start from a warning to novices. This point of view is based on the experience of having to deal with fuckups from novice developers, who were trying to use and abuse the language to the full extends of their abilities. Including fuckups of my own.
In this case I think 20 years of C++ experience doesn't help much. At least it doesn't help me. This is a challenge for procedural programmers to really think through what functional primitives are all about by implementing them in a procedural language. It's not the usual template metaprogramming brain teaser.
I very much agree. I should have stated more clearly that "20 years of coding" requirement in my post should include software (and hardware development) in a variety of languages, including functional languages. Or even domain specific languages like SQL, Matlab or VHDL/Verilog.
Years of experience is also hard to define. It's as precise a metric as lines of code are.
I've seen people saying 10 years of experience when they're closer to have 10 times one year of experience since they continuously do the same thing over and over. I've also seen people with barely a year or two of experience already coding circles around people with 10+ years.
It's either an exaggerated academic demonstration or a joke, but it's convincingly presented in a serious tone. Nobody genuinely thinks it's a good idea to replace a 9 line C function with 156 lines of templated C++ code (not counting the header full of templates classes it includes).
A coroutine is created once and it yields control to its "caller" each time it produces an item. To implement a coroutine, you only need to save its stack (local variables, instruction pointer).
He created a lazy stream: instead of yielding control for each item produced, it makes a linked list of items where each node is created on demand.
Thus, coroutines only return the "next" item, while streams compute and store the history of the whole computation.
Not to nitpick but you are talking about generators which IIRC are also incorrectly called coroutines in the Python community. A proper coroutines are not limited to yielding control to the caller, but anywhere.
I've described what's usually known as asymmetric coroutines or semicoroutines. "Asymmetric" because [as claimed by Lua manual at least] there are two separate calls for control transfer (yield and resume). Generators are less powerful.
Well, are you suggesting that a real C++ developer wouldn't think it is a good idea to include a library or framework in his program? (because 156 lines of extra code is too much?)
Have you ever used STL? It is full of template classes! It is a LOT of code in there.
The author implemented a stream processing framework based on lazy evaluation and an interface taken straight out of functional programming (Haskell) approach.
Tidy it up, make it more robust and why wouldn't it be industry strength and ready for production code?
#include <lazy_streams>
//3 lines of code to implement the original problem
What would be wrong with that?
I think C++ more than (m)any other language(s) sufferes from not-invented-here syndrome. If something is not in core language or STL then it might as well not exist -- people will always do their own thing from scratch. Or you grab some behemoth library like Boost which should cater to all your needs.
In contrast in Haskell/OCaml/Clojure (probably others too but those I'm most familiar with) one can download a tiny library that allows to radically change the control flow of the program.
What's demonstrated here is one such thing. It is based on sound principles (monads, functors...) and lazy streams which is a very well researched topic.
Sure, this is C++ we are talking about and there sure will be dragons somewhere. But if Bartosz ever dedicates his time to make a full fledged and tested library I will be damn sure to use it!
I....do not grok article. I don't use function pointers and jump tables all that much, but I'm pretty sure you could add a function pointer argument to printNTriples and replace the printf() with it and get somewhere if you really needed to get there. Starting to feel like I might be getting to the "Get off my damn lawn, kid!" part of my life.
> I'm pretty sure you could add a function pointer argument to printNTriples and replace the printf()
You could, but the control is still driven by printNTriples. What if you had to break early, before generating all N triples? In this simple case, you could use a return value from the callback to decide whether to continue or not.
In a more general case, you would you would encapsulate the state into a class (for example, the x,y,z,i locals from the original function would become instance memebers.)
It’s both a demonstration of the expressive power of C++—“In C++, you can say anything!”—and a joke—“…but it takes a very long time to say.” Several of my coworkers experienced with C++ are jumping ship to Haskell for new code, because C++ is becoming simply untenable.
I’ve been working in C++ for about ten years, since a couple years after I started programming. It’s certainly gotten better over time—particularly in terms of containers and ownership semantics—but it’s still not good exactly. For instance, it’s possible for mortals to write exception-safe code now, but still not memory-safe code.
I spend an inordinate of time language-lawyering for my coworkers to avoid future debugging hell, and though in my opinion I don’t even know the standard all that well, I’m certainly among maybe a few dozen people at my company who come close to knowing it in full.
It’s just an absurd source of artificial expertise that needs to die.
I suppose my main gripes are that moving is not free, you can reuse moved-from objects, and there is no story for checked borrowing of unique_ptrs—you have to use raw pointers or weaken the ownership semantics to shared_ptr so that you can use weak_ptr for safe borrowing. Not to mention that shared_ptr is often semantically questionable and used as an inefficient alternative to a garbage collector.
Yes, even I kind of agree that the language still has loads of baggage(read: warts) it could do without. "artificial expertise" got me thinking for a while, frankly :)
Also, I think Rust has kind of nailed ownership semantics - makes my life a lot easier when the compiler assists me with those things.
Well, before it was "any complex program contains bug-ridden implementation of Lisp", and now it's "any C++ program invents verbose Haskel".
But seriously, while this is toy example, it demonstrates what perhaps will be viable for standard library or for the future language features. Have you tried to read STL implementation code? - it's pretty incomprehensible, but it's still extremely useful library.
You do realize that this is a pretty common pattern in asynchronous programming, right? E.g. in the browser networking stack. Like the one that you used to post your comment. Which was most likely implemented in C++.
I'm not sure why this comment thread is so awful. It's as if veteran C++ devs are scared of Haskell or something. Lazy evaluation is a great boon for achieving a mix of performance and flexible coupling.
In a lot of simpler, lower-performance cases you _can_ get away with just evaluating the same function with the same inputs repetitively, which doesn't require these tools, but having the "next step" available makes it possible to apply that strategy in more places.
Not scared of Haskell, just we have other ways to do stuff like this in C++, namely iterators. I posted a solution using iterators here: https://news.ycombinator.com/item?id=7626984
My solution completely avoids dynamic memory allocation--even the stack allocation is just a handful of bytes per generated triple.
You're right that lazy evaluation is useful for performance, but TFA acknowledges that its own implementation is not efficient because it dynamically allocates memory repeatedly. This is not something we can hand-wave away: it's at the core (no pun intended) of high performance in C++ and many other languages.
Being a 100000 year old lycanthropic C coder things like lazy evaluation, garbage collection etcetera make my head spin (a full 1440 degrees) ..
But I have come across Milewski before since I started on C++, he has a clever way of explaining programming concepts while tending to avoid the usual "is a, has a".
A couple of months ago I gave Haskell a decent go, and I sort of got my head round the basics of being a lazy coder.
Take a never ending list:
let t=[1..]
Okay, fairly compact notation. I think of this code as declaring blueprints of the list in order to create it when someone demands it.
That's alright, as long as those blueprints take up substantially less space in memory than the list itself. In the example above, the entire list can be generated by a starting value and a function to append the previous value incremented by unity to the end of the list; the data exhibits the ability to be compressed infinitely. The same can be applied to many other simple lists, like the set of all square numbers. This is an efficient use of memory.
When the function required to create a certain list becomes comparable in size to the resultant list, this advantage is lost. Luckily, those lists are fairly rare - white noise would be a pathological example.
Well, Haskell has rather helpful strictness analyzer in its compiler that tries to determine automatically where strictness has to be enforced. C++ implementation does not have this advantage, but a limited form of laziness still seems to be a good tool for code expressiveness.
Some commenters seem to be offended by the way Bartosz presents his insights. Maybe some background is necessary. Bartosz has been pushing the C++ community for some time now to think more functional and consider the excellent properties of functional languages. His posts are great and his talks are very enlightening. I recommend his recent talk "I see a Monad in Your Future" [0] and I'm looking forward to his talk at C++Now.
Many of Bartosz' discussions focus on concurrency. It is one of the big and interesting problems we have at the moment. The criticism repeated here that "No one will ever be able to debug it" and the like is exactly what Bartosz sees in our future if we continue to program concurrent systems the way we do. He favours adoption of functional programming concepts to manage the complexity of concurrent systems. And I agree.
If code similar to the code in his blog is not acceptable in production code, a code review should make sure it never reaches production. An informed discussion about the pros and cons of such code could ensue during the code review, new insights are gained and everyone walks away a better, more knowledgeable programmer. I suspect this is one of Bartosz main goals.
39 comments
[ 2.9 ms ] story [ 89.7 ms ] threadSeriously. I like experiments, but just keep this away from production code.
An article like that should start with: "unless you really really know what you are doing and are an adult (20+ years of coding) you should probably avoid using techniques like that in the production code."
Experts can use them for disastrous consequences as well. This is C++ we're talking about here!
Although having over 7 years experience as a c++ programmer, since I'm not using it so much now I haven't managed to keep up so much with the changes in c++11 and feel a bit noviced overnight.
Still, move constructors are pretty handy so that's one change I use whenever I can.
The example of the article was simple of course. But solving the problem of printing Pythagorean triples was not the aim of the article. The aim was to show what makes lazy streams tick, to show that they can be implemented in C++ and that an uniform interface can be provided with functor, monoid and monad. Finally, the simple problem was solved with the new tool to show the inversion of control.
PS. std::vector is a stateful functor.
But, the mail point of the post was, that if you write an article like that, it should start from a warning to novices. This point of view is based on the experience of having to deal with fuckups from novice developers, who were trying to use and abuse the language to the full extends of their abilities. Including fuckups of my own.
PS. std::vector is a container.
I've seen people saying 10 years of experience when they're closer to have 10 times one year of experience since they continuously do the same thing over and over. I've also seen people with barely a year or two of experience already coding circles around people with 10+ years.
Isn't that just C++ in general?
https://github.com/BartoszMilewski/Okasaki/blob/master/Tripl...
A coroutine is created once and it yields control to its "caller" each time it produces an item. To implement a coroutine, you only need to save its stack (local variables, instruction pointer).
He created a lazy stream: instead of yielding control for each item produced, it makes a linked list of items where each node is created on demand.
Thus, coroutines only return the "next" item, while streams compute and store the history of the whole computation.
See http://www.lua.org/pil/9.1.html
Have you ever used STL? It is full of template classes! It is a LOT of code in there.
The author implemented a stream processing framework based on lazy evaluation and an interface taken straight out of functional programming (Haskell) approach.
Tidy it up, make it more robust and why wouldn't it be industry strength and ready for production code?
What would be wrong with that?I think C++ more than (m)any other language(s) sufferes from not-invented-here syndrome. If something is not in core language or STL then it might as well not exist -- people will always do their own thing from scratch. Or you grab some behemoth library like Boost which should cater to all your needs.
In contrast in Haskell/OCaml/Clojure (probably others too but those I'm most familiar with) one can download a tiny library that allows to radically change the control flow of the program.
What's demonstrated here is one such thing. It is based on sound principles (monads, functors...) and lazy streams which is a very well researched topic.
Sure, this is C++ we are talking about and there sure will be dragons somewhere. But if Bartosz ever dedicates his time to make a full fledged and tested library I will be damn sure to use it!
You could, but the control is still driven by printNTriples. What if you had to break early, before generating all N triples? In this simple case, you could use a return value from the callback to decide whether to continue or not.
In a more general case, you would you would encapsulate the state into a class (for example, the x,y,z,i locals from the original function would become instance memebers.)
I’ve been working in C++ for about ten years, since a couple years after I started programming. It’s certainly gotten better over time—particularly in terms of containers and ownership semantics—but it’s still not good exactly. For instance, it’s possible for mortals to write exception-safe code now, but still not memory-safe code.
I spend an inordinate of time language-lawyering for my coworkers to avoid future debugging hell, and though in my opinion I don’t even know the standard all that well, I’m certainly among maybe a few dozen people at my company who come close to knowing it in full.
It’s just an absurd source of artificial expertise that needs to die.
It seems to me that you barely have to think about memory ownership anymore ever since unique_ptr and shared_ptr came along.
Also, I think Rust has kind of nailed ownership semantics - makes my life a lot easier when the compiler assists me with those things.
But seriously, while this is toy example, it demonstrates what perhaps will be viable for standard library or for the future language features. Have you tried to read STL implementation code? - it's pretty incomprehensible, but it's still extremely useful library.
In a lot of simpler, lower-performance cases you _can_ get away with just evaluating the same function with the same inputs repetitively, which doesn't require these tools, but having the "next step" available makes it possible to apply that strategy in more places.
My solution completely avoids dynamic memory allocation--even the stack allocation is just a handful of bytes per generated triple.
You're right that lazy evaluation is useful for performance, but TFA acknowledges that its own implementation is not efficient because it dynamically allocates memory repeatedly. This is not something we can hand-wave away: it's at the core (no pun intended) of high performance in C++ and many other languages.
But I have come across Milewski before since I started on C++, he has a clever way of explaining programming concepts while tending to avoid the usual "is a, has a".
A couple of months ago I gave Haskell a decent go, and I sort of got my head round the basics of being a lazy coder.
Take a never ending list:
Okay, fairly compact notation. I think of this code as declaring blueprints of the list in order to create it when someone demands it.That's alright, as long as those blueprints take up substantially less space in memory than the list itself. In the example above, the entire list can be generated by a starting value and a function to append the previous value incremented by unity to the end of the list; the data exhibits the ability to be compressed infinitely. The same can be applied to many other simple lists, like the set of all square numbers. This is an efficient use of memory.
When the function required to create a certain list becomes comparable in size to the resultant list, this advantage is lost. Luckily, those lists are fairly rare - white noise would be a pathological example.
I wasn't aware of that, cheers for the heads up.
Many of Bartosz' discussions focus on concurrency. It is one of the big and interesting problems we have at the moment. The criticism repeated here that "No one will ever be able to debug it" and the like is exactly what Bartosz sees in our future if we continue to program concurrent systems the way we do. He favours adoption of functional programming concepts to manage the complexity of concurrent systems. And I agree.
If code similar to the code in his blog is not acceptable in production code, a code review should make sure it never reaches production. An informed discussion about the pros and cons of such code could ensue during the code review, new insights are gained and everyone walks away a better, more knowledgeable programmer. I suspect this is one of Bartosz main goals.
[0] https://www.youtube.com/watch?v=BFnhhPehpKw