I've been programming for years with primarily-imperative languages (C++, C#, javascript, python), but have recently experimented with some functional langauges (Lisp, Haskell) and was excited to try applying some of the declarative-style programming ideas in C++. I have a custom range-based STL replacement library I wrote a while back that made a lot of that possible in a fairly clean way.
Here's an example - a function to see if a target substring exists inside of a source string, ignoring case. First the plain-ol' imperative way:
bool StringContains(const string& source, const string& target) {
// Figure out search area, exit if target is too big to exist in source
if (target.size() > source.size()) {
return false;
}
size_t endIndex = source.size() - target.size();
// For each potential position...
for (size_t i = 0; i <= endIndex; i++) {
// Check if target is here
size_t strPos = i;
bool foundHere = true;
for (char targetChar : target) {
char strChar = tolower(source[strPos]);
targetChar = tolower(targetChar);
if (strChar != targetChar) {
foundHere = false;
break;
}
strPos++;
}
// If found here, return true
if (foundHere) {
return true;
}
}
// If not found by now, return false
return false;
}
And here it is using my declarative library (which use some C++11 magic):
bool StringContainsDec(const string& source, const string& target) {
// Figure out search area, exit if target is too big to exist in source
if (target.size() > source.size()) {
return false;
}
size_t endIndex = source.size() - target.size();
// For each potential position...
auto targetRange = All(target) | Transformed(tolower);
for (size_t i = 0; i <= endIndex; i++) {
// If found here, return true
auto sourceRange = All(source) | Sliced(i, i + target.size()) |
Transformed(tolower);
if (RangesMatch(sourceRange, targetRange)) {
return true;
}
}
// If not found by now, return false
return false;
}
A little more compact and perhaps English-like and readable, which is nice. The "|" is analogous to a shell script pipe, routing values thru to the next operation. So:
All(source) | Sliced(i, i + target.size()) | Transformed(tolower)
means, set up a range that, when iterated, will take each character of 'source', sliced between index i and i + target.size(), and pass each character through tolower().
RangesMatch() iterates each of the two ranges and returns true if each element matches.
So, that's all fine and good, and it works correctly. But over time I've found, experimenting with this approach in practical situations:
- The declarative code is harder to debug. With the imperative, you can just step through in the debugger, line by line, and see what's going on. With the declarative, it's not that much more complex, but you need to jump through some different library functions of constructing the range, calling the internal iterator functions (Front(), PopFront(), etc.) etc. So it jumps you around from place to place, making it more confusing to track the logic. I imagine this is easier in e.g. a Lisp debugger.
- The declarative code is a bit slower. On my system it's about half the speed of the imperative code. The ranges are lazily constructed and very efficient, and only allocate locals on the stack, but it still involves a little more under the hood, like tracking start/end pointers, which adds up in nested loops etc. With declarative it seems like you can easily lose touch with what your code is actually doing. If you have a huge chain of operations you'll miss opportunities to simplify, save useful intermediate values so they don't need to be recalculated later, etc.
- The declarative code is harder to modify over time, I find. If I want to do some extra operation on each character, I need to add another transform function, or lambda etc. In imperative programming I just add a line of plain ol' code inside the loop, or 100 lines if needed, and it's fairly easy to follow.
- I find the imperative style more intuitive as I'm writing. It better reflects the order that things happen, let's me proceed step by step without having to juggle the whole thing in my head up front, etc.
Now all this stuff might be particular to my implementation or my preferences, but I imagine some of it is inherent to the style too? This string function is just one example but I've found it with all kinds of things when I implement both side by side - that 80% of the time imperative style wins for me, just do it with plain old loops and if statements rather than messing around with higher-order functions, map/reduce, etc. They may add some code brevity and a little less typing if your text editor sucks, but in complicated real-world situations they become confusing and harder to maintain.
So is declarative overrated? Has anyone had broad experience with both approaches, especially with complex real-world projects in functional languages? Curious to hear what other people think.