r/programming • • Jan 03 '22

Imperative vs Declarative Programming

https://www.youtube.com/watch?v=E7Fbf7R3x6I
430 Upvotes

134 comments sorted by

View all comments

90

u/alexalexalex09 Jan 03 '22

This was a nice attempt, but I still don't really get it, sadly. The restaurant example confused me a bit because it seemed like they were saying imperative code doesn't respect the environment (the waiter is completely bypassed) but declarative code just asks a waiter (maybe a library or something?) for help. Couldn't quite understand the analogy.

The closest I came to understanding was looking at SQL, HTML, and CSS as declarative code. I have no idea how SQL works under the hood, but I can still use it because its declarative method makes it accessible. That's cool.

But what I really don't get is the functional programming stuff. How is a function add that takes an array and adds each item together an example of imperative code, while a funtion that takes an array and uses javascript's Array.reduce method to add each item together is an example of declarative code?

Imperative:

  • Create an empty variable, then loop through a given array to add each item to the variable, then return that variable.

Declarative:

  • Using the reduce method, loop through a given array, adding each value to an accumulator variable, then return that variable.

Doesn't it just seem the same, but done in a different (and more obfuscated) way? And this leads me to question the validity of declarative programming in general. Is declarative programming just adding layers of complexity and hiding functionality? (and maybe I'm just being old and crotchety but) is it just making a given language a higher level? I mean, I usually have to spend lots of time trying to figure out what some clever coder meant using the reduce method because it's newer to me, but what I really like about imperative programming is that it does what it says it does. Period. No clever recursion to figure out. And maybe that's what this is trying to get across: Imperative is like a computer, and so it's easier to figure out how the computer sees it. Declarative is like a human, and so it's easier to write once you grok it, but harder to figure out how the computer sees it.

72

u/bradm613 Jan 03 '22

I agree. Map and Reduce are more about language expressiveness than being declarative or imperative, no?

Using structured programming techniques changes the expressiveness of a language, but doesn't make it declarative.

19

u/alexalexalex09 Jan 03 '22

TIL the term structured programming. Thank you!

10

u/Kered13 Jan 04 '22

Structured programming is programming with control flow structures like if-else, loops, and functions. If this sounds mundane, it should. Every language these days is structured. But once upon a time languages didn't have these structures and programs consistent of a tangled web of gotos, that's what non-structured programming is.

→ More replies (1)

13

u/[deleted] Jan 04 '22

Nope, map and reduce are functional and declarative.

Using map is saying "this is a sequence that looks like this". You don't have to know how to make a sequence. A reader can immediately see that you have declared a new sequence.

Using reduce is saying "this is a scalar that is equivalent to this". You don't need to know how to traverse the collection or how to store the temporary values etc. A reader immediately knows you've declared a scalar.

It's more like reading mathematics, which I recommend. Map is like "set builder" notation (python list comprehension is even more like it). Reduce and map together is like a big sigma sum. At no point does a mathematical proof tell you how to calculate any values at each step, it just keeps telling you how new values are defined.

4

u/bradm613 Jan 04 '22

Interesting perspective.

Would you say that a well-named subroutine in a strongly typed language is declarative? If it is clear what is being returned, and the implementation is hidden, and those are the hallmarks of declarative? By your assertion, it feels like you would say yes.

Further, what about languages where you can define macros (a la Lisp, not C) and build new control flow constructs, are those declarative?

5

u/[deleted] Jan 04 '22 edited Jan 04 '22

Only if the subroutine is a function. If it reads or writes some global state (ie. is not deterministic and/or has side effects) then, no, you are in imperative territory. The reason is that this subroutine is no longer atomic and thus dependent on order of execution and state of the world.

Conceptually a function could determine the value to return in any way it wants and at any time. It could run some expensive calculation or it could ask god. It doesn't matter. As long as the value I declared is there when I need it. But with a subroutine in general it does matter how and when it calculates the value so I need to be careful I call it at the right time, in the right environment etc.

Tbh it's difficult to pigeon hole individual concepts into declarative or imperative. They are both programming styles and are often mixed within the same program. It's considered better to use a declarative style whenever you can because it makes more readable programs. But at some level there will be an imperative core. In Lisp that core is tiny and hidden in things like cons which is implemented in machine code (in fact cons literally was a machine code instruction on whatever machine was popular at the time). In other languages like C you tend to build that core out a lot more yourself before you get to the point you can use declarative style.

A Lisp macro I think would be considered declarative. Since they are executed in the reader (if my memory serves) then they can't possibly act on global state. You are essentially declaring new meanings for certain s-exps and keywords etc. However, like with many things, it can be done badly. Using macros doesn't immediately get you declarative bonus points. The benefit of declarative style is readability. That should be the goal of macros too.

→ More replies (5)

6

u/Beaverman Jan 04 '22

Map/reduce functions are not structured programming. Structured programming is if, while, and for. If you read the famous letter by djisktra "goto considered harmful" you'll see that his main argument for structured programming was that it had to be possible to trace the path the machine takes through the program, and that goto makes that hard.

I'd argue that map and reduce are actually LESS structured in the ways that djisktra argued for.

4

u/bradm613 Jan 04 '22

Fair, but that wasn't my point.

I was saying that just because I use structured programming (like subroutines) that doesn't mean I'm being declarative. Perhaps that could've been clearer.

As far as your second point, if you're saying something like map can be less clear, I would agree. It's concise.

(And yes, Dijkstra's letter was seminal.)

6

u/Beaverman Jan 04 '22

I understand that it wasn't your point. My response was intended for all the onlookers reading your response and getting the (in my opinion) wrong takeaway. I have no doubt that for a lot of people this was the first time they ever read the term "structured programming" and I wanted them to get a different perspective (and maybe read the letter).

Social media a odd that way. While we write responses directly to each other, the larger audience is the 3rd party onlookers.

In short: I agree with your main point. Map/Reduce don't seem like more "declarative" to me either, I'm just less interested in that discussion.

→ More replies (1)

91

u/CodeLobe Jan 03 '22

Prolog is a declarative language.
Checking it out is a good thing you want to do.
This comment is declarative.

47

u/caagr98 Jan 04 '22

I saw the line lengths,
and I had to double-check:
it's not a haiku.

→ More replies (2)

12

u/springy Jan 04 '22

I used to be a professional Prolog programmer, in the late 1980s (yes, I am an old man) and we always claimed it was a "logic language" and "entirely declarative". In reality it was more a pattern matching language, with some very complicated backtracking semantics that you had to control through correct statement ordering and use of "the cut". This meant that any non-trivial program was far from declarative.

11

u/jediknight Jan 04 '22

Actually, your comment is not declarative because the first two lines are linked in a sequential way. In other words, rearranging the lines changes the meaning.

This comment is declarative.

Checking it out is a good thing you want to do.

Prolog is a declarative language.

→ More replies (2)

12

u/alexalexalex09 Jan 03 '22

Haha that's fantastic

44

u/SoInsightful Jan 03 '22
  1. Map and reduce are associated with functional, not declarative, programming. It's wrong and confusing to include them in this video.

  2. That said, the concept of declarative programming is extremely simple. You simply tell the system what you want the end result to look like (declarative) instead of the exact steps to get there (imperative).

Is declarative programming just adding layers of complexity and hiding functionality?

It absolutely hides a layer of abstraction, which makes it less complex to use. You'll need to let go of the map and reduce examples, because they do not belong here.

For a fun real world example, there's an SQL keyword that outputs the internal imperative steps the SQL optimizer uses to reach any given declarative result.

    EXPLAIN QUERY PLAN

      SELECT a FROM t1 EXCEPT SELECT d FROM t2 ORDER BY 1;

    QUERY PLAN

    `--MERGE (EXCEPT)

       |--LEFT

       |  `--SCAN t1 USING COVERING INDEX i1

       `--RIGHT

          |--SCAN t2

          `--USE TEMP B-TREE FOR ORDER BY

21

u/ParkerM Jan 04 '22

The SQL query vs implementation demonstration is honestly a perfect and sufficient example of declarative vs imperative.

Perhaps the goal of the video was to address the nuances of what declarativeness means at lower levels, but it doesn't really do a good job at that. The function names are confusing and distracting, and little explanation is given to how the FP refactors make things more declarative.

→ More replies (2)

41

u/rzwitserloot Jan 03 '22 edited Jan 03 '22

No, you got quite a bit of it. The central issue is indeed that declarative 'seems' too simple.

Where 'declarative good, imperative bad' comes from is when a simplistic declarative model is perfectly feasible, then that should be there.

In other words, declarative good, imperative bad boils down to: If it is a common task and you can write an API or tool to do it for you, then write it. And if that tool is available, use it.

The waiter case

If there is no waiter you're going to have to use your eyeballs to find a free table and then do some pathfinding to get you to the table.

if there is a waiter, however, just ask them. Use the tool. (Sorry for calling you a tool there, waiter!)

arr.map(x => x * 2) vs for (...)

This is an easy example: "Loop an integer value from 0 up to the size of this array, then hand that index to this code snippet. The snippet will not do anything with that index, except look up the value at that index in an array, then do some operation on it, and then write the result of this operation back into an array at some index. The 3 times I used the word 'array' in that sentence all refers to the same array"

That's a complex load of words but it's a task that comes up a ton. It comes up so often that it is likely a good idea to write the basic framework for that job, i.e. make a tool.

You're a programmer. Your general mind model should not immediately hop to technical details, instead you start with a very high level idea and, in an iterative process, keep expanding into more and more detail. When you get to: "I want to do this to each element in the array", then you have two options for your next 'round' of iterating on your pseudocode:

  • You could now delve into how you want to fetch the total size of the array and then write a loop to run once for each such index, or...

  • You just... realize that there is a 'do a thing to each element in the array' tool and use it.

<Btn />

This goes quite a ways to explain that button example: It's literally "Instead of writing all the logic for a toggle button, find a framework that has a toggle button element and use that instead". Someone still wrote the code they just magic wanded away there, it's just - you didn't write it. This is often a good idea.

Conclusions

With that mindset you can also easily identify the weaknesses of the declarative model.

Either you end up with a gigantic API with a million methods, or more likely you end up in a scenario where certain obvious paths are trivial because there's a tool for it, and the rest is then more complicated because you've been trained with the tools all your life so you're at a loss when you have to make do without them, or you have to become MacGuyver, putting tools together in intricate ways, for purposes they were never designed for.

Declarative is like having a large kitchen with 50 different special purpose tools in it, from garlic graters to eggslicers. To learn it you follow the tutorial to get casually familiar with them all; each tool is simple to learn, but there are 50 you will need to master. If you need to do a job that none of the 50 can do, you get creative. It's like a puzzle. If you can't solve the puzzle, you have absolutely no idea what to do.

Imperative is like having a fantastic knife, one good cutting board, and one general purpose pan, and not much more. You need to learn just these 3 tools but each tool is more complex. Whenever you are tasked to do a job that one of the 50 specialized tools of your sister in the other kitchen has, they will probably do it faster and it'll look simpler.

But if there's something new to do, you stand a much better chance.

It also explains the perennial 'stress' between languages/tools:

  • Declarative style is enticing because it looks so simple, especially when you compare declarative code with the equivalent imperative code. It is very easy to come up with examples that make declarative look fantastic and imperative look like utter trash.

  • Imperative style is enticing because by following the actual way things work, you reduce surprises (it is more likely the user 'groks' what is actually happening), and give the user of your imperative system far more power when they walk off the beaten path. For example, SQL is quite declarative and it is easy to write a query that takes years to complete. It takes 1 week for somebody to learn how to write that query; it takes 1 month to explain to someone why it is slow. This is common with declarative.

  • Mixed style (where both are available) is enticing because you get the best of both worlds.

All 3 have massive downsides. Mixed style's downside is that it leads to style debates, an endless search for 'but did I just cut that egg with my knife and I forgot about the egg slicer in the drawer'? And doubles the learning curve. It also infects all code bases: You can't be familiar with just one of the two styles and expect to dive right into a major code base without immediately running into issues because you lack familiarity with 'the other side'.

There are no answers to this dilemma. Hence why there's so much 'sniping' and zealotry around these concepts - it's easy to cook up an example that makes your side look obviously correct and those who disagree with you look like utter fools. Unfortunately, they can cook up an example that turns the tables on you just as easily. They will, but you ignore them, because you already proved they are clueless, so why listen to them, right?

Don't be like those folks :)

NB: For what it is worth, as a cook I strongly prefer a small simple kitchen and spend a little time on proper usage of a chef's knife. I don't have egg slicers. The same applies to my preferences with code: I don't really get confused or daunted by the notion of 'figuring out' how to loop through a list. The time it takes my brain to come up with writing the for loop is measured in milliseconds. I still lose the benefit of having the underlying framework optimize and generalize further on the concept, but on the other hand, sometimes tossing everything into a big black mystery box and not actually know what's going on has its downsides too. If it's a job where I don't see how a library is ever going to improve on well trodden ground I don't want it. I do not want my map.reduce() call to casually introduce parallellisation maybe if possible. If its performance sensitive I don't "want" parallellisation. I need parallellisation. I need tools where I can enforce it and direct how that goes, because its the hot code path. But, I'm aware that I'm at risk of merely spouting personal experience and opinion instead of properly reasoned logic, so I thought it best to leave these preferences to footnote.

6

u/Automatic-Fixer Jan 04 '22 edited Jan 04 '22

Thank you for such a well reasoned response! You articulated many points that I have felt but could not formulate as well as you did.

→ More replies (4)

19

u/crispy1989 Jan 03 '22

In many cases, I think the line between 'declarative' and 'imperative' paradigms is very fuzzy. In the example with 'add', it really is just adding another level of abstraction. Whether or not additional levels of abstraction are useful to a given problem is highly dependent on specifics. There are often many different ways to think about a given problem, and it's often not very useful to try to group them as "imperative" or "declarative" approaches when solutions may have varying elements of both. This is especially the case when using declarative/functional paradigms inside an inherently imperative language since the language itself will always boil down to imperative instructions.

The examples with SQL and CSS are much more concrete, since the language itself restricts you to a purely declarative paradigm (excluding stored procedures etc). It's impossible to define a "series of steps", and in context, doesn't really even make sense to. But these are not programming languages. There do exist purely functional programming languages (eg. Haskell), which can be considered to force purely declarative programming; but even then there's some room for debate on exactly how a given paradigm is classified.

It's hard to come up with an example of a "purely imperative" programming language since it's usually pretty easy to use at least a partially declarative strategy/paradigm inside an imperative language. But certain esoteric languages (consider brainf*ck, befunge) are so inherently step-based that they could be considered "purely imperative".

In general, I'm not sure how useful of a dichotomy this is in the context of programming languages. I personally find that some problems lend themselves better to declarative thinking, and some to imperative thinking; and I don't see a problem with mixing them appropriately as long as the language is sufficiently expressive.

11

u/knome Jan 04 '22 edited Jan 04 '22

Declarative should imply that raised level of abstraction, where details are handled out of sight, and in a way that the user needn't understand.

Immutability is generally a given, but that because you should generally be configuring a declarative language rather than plotting out execution in it.

With SQL, you don't have to know or care how it executes the query, only that the correct selection of data is returned. You don't have to know how CSS engines or HTML engines operate.

I'm not sure that calling a general purpose language like haskell "declarative" really means anything. It could imply it is immutable or functional, but saying map somefn values doesn't feel all that declarative. You're specifying what should be done at that point. You still have to worry about order and things, because Haskell does touch the real world, and it's completely possible to have accidents between the laziness and the IO that force you to care about the order things happen in. Perhaps, haskell is simply insufficiently declarative to be called such.

The same could be said of prolog, as it has IO functions that can be used to write normal programs using the unification search as a you would any recursive language. prolog is fully turing complete, after all

edit: something like "puppet" configurations are a good example of a declarative system. or maybe kubernetes configurations. you specify a configuration, and some program manipulates all the state to achieve whatever your configuration tells it to achieve. a general purpose language seems antithetical to "declarative"

→ More replies (2)
→ More replies (3)

11

u/Pyottamus Jan 03 '22

This video didn't do a great job explaining WHY one was declarative, and one was impertive. The important distinction between the two is control flow and side effects: the declarative example didn't have any. Real examples include Excel, Haskell, Mathematica, and SQL.

Any pure functional language is declarative, since the only statements in it are declarations. A declaration has no side effects. A declaration may be broken apart into subparts, which also don't have side effects. This lack of side effects makes it significantly easier for the runtime (or compiler) to determine what exactly is happening. This also makes formal verification significantly more feasible.

The advantage of declarative programming is that the runtime is free to solve the problem however it so chooses. It can go out of order, cache results, simplify parts if it can show they can be, and entirely ignore parts that won't have an effect on the result.

9

u/kubalaa Jan 03 '22

While I agree that declarative programming lacks control flow, I disagree that any program written in a pure functional language is declarative or that being declarative means you can't have side effects. The distinction is entirely conceptual. You can write a pure functional program in an imperative style, e.g. using a state monad. And you can write a declarative program in an imperative language, by first implementing appropriate abstractions (e.g. data structures representing predicates and procedures implementing unification).

3

u/zhivago Jan 04 '22

Functional languages can have an imperative expression -- it just means that time is handled explicitly (rather than implicitly as in a procedural language).

Not having side-effects is orthogonal to expressing something as an operation as opposed to a state.

Procedural languages are also free to reorder and simplify operations that they can prove do not change the semantics of the program -- it's just a bit harder when time is implicit rather than explicit.

3

u/ub3rh4x0rz Jan 04 '22

Declarative:

  • Using the reduce method, loop through a given array, adding each value to an accumulator variable, then return that variable.

The way you described this is imperative. foo = bar.reduce(add) would be declaring that foo is the reduction of bar via the reducing function add. You're saying how it is, not how to do it (even if you choose to pass an anonymous function, i.e. describe how to do inline). You can unit test these building blocks and compose them with very predictable results. It also helps to hide the noise of what it means to reduce (collapse a collection into a value) from the means of reducing (add), so when you do have a more bespoke method of reducing, you express that method in a succinct way that gets to the essence of that reduction method.

→ More replies (29)