4

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.

closed as primarily opinion-based by gnat, Bart van Ingen Schenau, Scant Roger, Dan Pichelman, Telastyn Jan 9 '16 at 14:29

Many good questions generate some degree of opinion based on expert experience, but answers to this question will tend to be almost entirely based on opinions, rather than facts, references, or specific expertise. If this question can be reworded to fit the rules in the help center, please edit the question.

  • 1
    I have quite some experience in Haskell and Scheme, and I find programming in C++ pretty cumbersome. The only reason I can see to keep programming in C++ is execution (not development) speed and the availability of some particular library (e.g. Qt). IMO it is better to learn declarative programming in a language that better supports it and then try to apply it in C++ or Java: trying to do both steps (learn and backport) at once is IMO more difficult. – Giorgio Jan 9 '16 at 7:38
  • It depends on the problem you are trying to tackle. It's not like they are interchangable in all scenarios. That one can be declared (ha ha) better than the other, given enough thought. We could say one is more "high level" than the other and that the other would not exist without the one. Declarative is like a conductor (or DJ), imperative is like a player. – Martin Maat Jan 9 '16 at 10:01
16

The declarative code is harder to debug.

I would say that is a function of the quality of your debugger. If your debugger understands the imperative constructs but not the declarative ones, then of course the delarative ones are harder to debug. But you could easily imagine a different debugger with different priorities, where the opposite is true.

There are some language designers which do care so much about tooling that they are even willing to let toolability influence the language design or even compromise on language features to facilitate good tools. The obvious example is Kotlin, which is designed by a tool vendor (JetBrains). The lead developers of Scala are also famously opposed to expanding Scala's type inference, not because they don't know how to do it (they do) or because it's hard to implement (it is, but they have smart compiler writers), but because they haven't figured out a way to implement it with good error messages. (Think mid-90s C++ template instantiation errors.)

The declarative code is a bit slower. […] With declarative it seems like you can easily lose touch with what your code is actually doing.

Yes. That is the whole point. That's why it's called "declarative": because you declare what you want to happen, not how you want it to happen.

This gives a lot more leeway for the compiler to optimize things.

There's a great example in one of the Supero (a supercompiler for Haskell) papers. The author compares a simple, expressive, declarative, purely functional, one-line word count function in Haskell (main = print . length . words =<< getContents), compiled with a combination of Supero, GHC, and YHC with a hand-optimized state-machine-based while loop in C, and much to his own surprise finds that the Haskell is marginally faster. How could that happen? Well, the compiler actually transformed the Haskell code into the same state-machine loop that the hand-written C version has, but it can do one additional trick that C (at least without inline assembly) can't: encode the state(s) in the program counter.

In your case, you have created a declarative DSL, if you will. But the C++ optimizer doesn't know anything about the semantics of your DSL, so it can't take advantage of the additional freedom.

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 […] lambda etc. In imperative programming I just add a line of plain ol' code inside the loop […].

I don't follow. Is there really difference between:

step1();
step2();
step2a(); // inserted later
step3();

and

transform1    | 
  transform2  |
  transform2a | // inserted later
  transform3;

I find the imperative style more intuitive as I'm writing.

There is no such (absolute) thing as "intuitive". Intuitiveness is all about familiarity. Remember the Star Trek Movie, when Scotty tries to use a computer with what we consider to be an intuitive user interface? He ends up trying to speak voice commands into the mouse.

A lot of people consider loops to be intuitive, and recursion un-intuitive. However, just a couple of months ago, there was a question in the Ruby tag on StackOverflow by a complete programming newbie, who had written code like this:

def main
  # do something
  main
end

To him, this was the intuitive way to do something over and over again. (And why not? "Do something, and then start again what you are doing" is a perfectly sensible mental model for what we imperative guys call a "loop", is it not?) And for a Scheme, ML, or Haskell programmer, this would be intuitive, and loops wouldn't. (In fact, a pure ML or Haskell programmer wouldn't even know what we are talking about, because their languages have no loops.)

Another example from me personally: as a Ruby programmer and fan of Smalltalk, I cannot understand why anyone would ever want a static AOT compiler. And yet, the C++ community cannot understand why anyone would ever want a dynamic JIT compiler.

Unless and until you have written the same amount of (serious, non-toy, complex, large, production-level system) code in both styles, the style which is more familiar will be more "intuitive". That's just the nature of things.

  • 1
    Why would anyone want a static AOT compiler? Because the hardware you're running on isn't likely to change, so why waste execution time recompiling, doing the same exact work with the same exact results over and over and over again, every single time you run the program? JITs are good for scripts, and (depending on the circumstances) for development time, but for deployed software, AOT wins every time. – Mason Wheeler Jan 9 '16 at 12:15
  • @Jörg Good reply, thanks. Re: debugging, I guess part of the issue is tracking intermediate results, which functional / declarative style tends to hide (which can be good). Not sure I agree on trusting the optimizer to do a good job or that functional code is inherently more "optimizable" than imperative though. I've seen good and bad machine code generated from both. And not sure it's good to lose touch of what the machine is doing - all software ultimately is about memory and its manipulation, and thinking in those terms can allow insights that greatly simplify code/architecutre complexity. – QuadrupleA Jan 9 '16 at 21:38
  • @QuadrupleA I think the equivalent insight for declarative programming is that "all software ultimately is about the efficient expression, generation and manipulation of ideal algorithms and thinking in those terms can allow insights that greatly simplify code/architecture complexity". The models are so different that what they consider "insights" are deeply different, but both are useful ways of thinking even if they seem opposed when you've only mastered one. – Racheet Sep 13 '16 at 14:32
  • As a long time C++ game programmer, I'd say that us C++ folk are more open to alternate execution these days. Compile time execution is allowing for some really nice things, and run time compilability (JIT) would be great too if we can make sure it's secure, and we don't have a bunch of machine/JIT version specific bugs to have to fight! (: – Alan Wolfe Nov 8 '16 at 23:57
4

IMHO declarative programming is very ambiguous.

In particular, it had different meaning in different countries (in France it is not the same as in the US) and in different times (before or after AI winters).

My understanding of it is that of Jacques Pitrat who speaks more wisely of declarative knowledge:

we give knowledge in a declarative form, which does not include how to use it.

In a certain way, declarative programming is a contradiction in terms; programming could be viewed as configuring computers on how to do things, but declarative means that the system should find itself on how to do things, and we declare only what we want it to do, not how to do it.

Once we have a system understanding declarative knowledge, "programming" it should be quite easy: we give separately a lot of declarative knowledge (including declarative metaknowledge about how to compile and use declarative knowledge) and some objectives and goals. This is also a dream of some AGI system, and J.Pitrat has written a lot on theses.

And programming might be extended to the notion of writing source code: the developer writes some formalization understood by some system. That source code is the preferred formalization for the developer (this is the definition of source code for free software enthusiasts).

Actually, there is a continuous spectrum between declarative and procedural knowledge....

So IMHO functional programming is not exactly declarative programming, but indeed functional languages are more declarative than procedural ones. Also, declarative systems are not overrated, but you need some dozen of years to develop them (read the mythical man month)

For a practical short term view, declarative programming may simply mean to favor data (including "declarative" configuration data giving some "goals") over code.

Read also about expert systems.

PS. I strongly recommend reading J.Pitrat's blog and books. He devoted his entire life to declarative knowledge.

  • 2
    In this case, the OP has given us a very specific example of what they mean by "declarative" programming, so imo this is answering a very different question from what the OP is asking. – Ixrec Jan 9 '16 at 13:16

Not the answer you're looking for? Browse other questions tagged or ask your own question.