Declarative vs Imperative in Functional Programming

preview_player
Показать описание
Functional programmers often point out that one of the big advantages of their approach is declarative programming. They sometimes position declarative vs imperative programming, but it's not quite as simple as that. This is not really about functional vs imperative, these are choices, and tools, that can be useful in most programming languages. So not functional vs OO or imperative vs declarative, but maybe all tools that we can use to do a better job.

In this episode, Dave Farley, author of “Modern Software Engineering” and “Continuous Delivery” explores the role and applicability of declarative and imperative approaches to defining systems.

_____________________________________________________

🔗 LINKS:

_____________________________________________________

📚 BOOKS:

In this book, Dave brings together his ideas and proven techniques to describe a durable, coherent and foundational approach to effective software development, for programmers, managers and technical leads, at all levels of experience.

📖 "Continuous Delivery Pipelines" by Dave Farley

NOTE: If you click on one of the Amazon Affiliate links and buy the book, Continuous Delivery Ltd. will get a small fee for the recommendation with NO increase in cost to you.

-------------------------------------------------------------------------------------
Also from Dave:

🎓 CD TRAINING COURSES
If you want to learn Continuous Delivery and DevOps skills, check out Dave Farley's courses

📧 JOIN CD MAIL LIST 📧

-------------------------------------------------------------------------------------

CHANNEL SPONSORS:

Рекомендации по теме
Комментарии
Автор

The complexity of map/filter/reduce comes from the learning curve, once you have learned it, it has become a part of your long-term memory, you will be able to process them intuitively in no time.

On the other hand, the complexity of loops, mutations, conditional branches can only be handled by your working memory because there is no abstraction to form patterns

wongyok
Автор

Another thing I wanted to say is that a `for` loop can do anything. When you see it you need to parse the code and you need to mentally jump from place to place where mutations occur.

When I see `filter` I know what it does and I know I can mentally replace this expression by its result (referential transparency) and carry on reading the next expression. There is actually a lot less cognitive load.

And you don't need to define the predicate as an anonymous function. Functions can have names and these names carry meaning. The whole point of higher order functions is to be able to pass *functions* to them. If you write the instructions on the `filter` call site, you only partially take advantage of HOF, because the predicate can also be a composition of other functions, with their narrow scope (== small cognitive load).

You can shove arbitrarily complex predicates in there. Try to do the same in your for loop and see if it's still easy to parse. And if your reaction to this is that you can use a function inside your for loop... well, congratulations, you just implemented filter ;)

ApprendreSansNecessite
Автор

5:56 Cognitive load is not absolute. Some idea might be more complicated to grasp but once you learned them, it could be easier afterward. For me, the cognitive load to go through what the loop does with every element is higher than just applying a filter.

OLApplin
Автор

I was always confused by both and never really understood it even after writing lots of code in different languages and also reading lots of articles about this. Your example was really good as you said that we don't know how "append" method adds at the end of list and how "filter" function filters the list so in a sense all programming languages are declarative and imperative at the same time. That made my concepts clear.

Thank you for clear crisp explanation.

ABTA
Автор

To me these discussions are always somewhat weird. I've always enjoyed finding elegant ways to solve problems and sometimes they end up being more declarative or imperative. I agree with the point that the design of the solution matters more than the language patterns hidden in the functions in most cases. Cognitive load and cyclomatic complexity are super important, but especially cognitive load changes over time when you get more used to particular patterns and styles. It really helps me if a codebase follows one style rather than multiple styles mixed when it comes to reading and understanding it.

WouterSimonsPlus
Автор

I agree with this take. Declarative programming is syntactic sugar. Declarative design (including executable specifications) is extremely useful for scaling software and encapsulating ideas.

manishm
Автор

Certainly an interesting topic, and one that’s very difficult to discuss effectively in a generalized way. The answer to “which is better” typically comes down to a “I know it when I see it” situation. Ideal declarative scenarios can often be beautiful in their elegance, but there are other concepts that are practically impossible to express in a declarative way, or at least become convoluted and very unintuitive when done so.

I do like the insight that declarative systems can often *start* beautiful and become ugly and cumbersome as features get added. It is very true to my experience.

Ultimately there’s one fundamental truth in programming: the most important thing is the ability to break the system down into units that are small enough to describe and comprehend. Everything else is secondary to this.

davewx
Автор

declarative_words(Dictionary, Words) :-
findall(Word,
(member(Word, Dictionary),
length(Word, 5)),
Words).

Couldn't let this go without the example in the most iconic of the declarative languages: Prolog. Read as "find all the Words such that the Word is a member of the Dictionary and the Word has length 5." How's your cognitive load on it? How Prolog actually executes it depends on which Prolog you're using.

Dr-Brown
Автор

One thing to point out is that the foreach construct is in itself declarative. In the example where word counts are compared, the use of a foreach loop (a more declarative approach) over a regular for loop (a more imperative approach) is reducing the word count by quite a bit. The code can be translated much more naturally to English precisely because we are using a foreach and not a for loop.

zkreso
Автор

In terms of cognitive load, I would say it kind of depends. Using the same example of length 5 words, here is the JS version (in FP-style).

const declarativeWords = wordlist => wordlist.filter(w => w.length === 5)

I would say that this is not only lower cognitive load, but also that it reads even more like English, "take the wordlist and filter it leaving only the ones with length equal to 5". Here is the Haskell version:

declarativeWords = filter ((== 5) . length)

This one just keeps the important bits. "We are filtering for the ones with length equal to 5". All the boilerplate is gone.

Going back to JS, here is the imperative version:

function declarativeWords (wordlist) {
const words = []
for (const word of words) {
if (word.length === 5) words.push(word)
}
return words
}

When I read this code I go like this:

- Hmm an empty array followed by a for, maybe a map?
- Let me look if we are pushing inside the look. Ok we are, so a map... wait
- The push is inside an if, so it's actually a filter.
- Are we modifying the value? Nope, just a regular filter.
- So we are filtering for words of length 5.
- Let me double check we are actually looping over the input array. Yeap, no surprises there.

- ...Well, if this was a filter why didn't you say so from the start!!!

The FP-style versions just flat out spell filter, I don't have to look for patterns there. Also, if I'm not interested in the details of the filtering I can bail right away (maybe I'm looking for changes in the shape of the data), the imperative version forces me to understand it fully before looking at something else.

Another point is the way that you perform changes on these functions. If the next step is to, say, make the words upper case. This is how the imperative version is usually going to be changed:

function declarativeWords (wordlist) {
const words = []
for (const word of words) {
if (word.length === 5)
}
return words
}

With this it looks almost like a filter, but now it's also changing the data, so reading this code for the first time you have to understand it fully to see where the data is changed. Here is the natural change to the FP code:

const declarativeWords = wordlist =>
wordlist
.filter(w => w.length === 5)
.map(w => w.toUpperCase())


The 2 parts correspond to 2 separate function calls.

To me this code is just lower cognitive overhead overall because it's easier to know which parts to ignore when browsing around (browsability is really important because code is read more often than written), and because the patterns are not obscured by loops, assignments, control flow, etc., but rather spelled out in words.

(To be clear, the imperative code is a bit faster than the FP one, since in the FP version I'm creating more temporary arrays. And in general, to avoid mutating data structures, you'll end up with slower code that does a bunch of extra copying. To me that tends to be a non-issue, since in my experience it is rarely a bottleneck.

There is also workarounds to these problems, like how Haskell optimizes this using stream fusion or how C++ and Java handle this with "streaming" semantics.

There is also various techniques that exist for immutable data structures sharing storage when being "modified", which reduces the amount of copying, like how Clojure does it.)

kebien
Автор

You said, "error of my ways". However, I beg to differ. You are very deliberate with your choice of words. Your arguments are cogent and well thought out. Your knowledge is steeped in decades of real world experience. I hold your insight in the highest regard, right up there with the likes of Ed Yourdon, Weinberg, Booch, Demarco and Fowler, just to name a few.

esra_erimez
Автор

From my perspective, i’d argue the declarative example you used actually requires fewer language constructs as it’s mostly just function composition. The more imperative version requires you to understand variable assignment, for loops, conditional control flow, list mutability. I think it’s easy with the curse or knowledge to assume these things are trivial and require less cognitive load, but that’s largely a matter of exposure.

hughdavidson
Автор

I think the problem is that we, as software developers, like to focus on the "how?" rather than on "why?" and "what?" which really matter in my opinion.
By saying "how?" I mean what is your IDE? What is you language? Are you using style? Are you using dark theme?

bleki_one
Автор

Thank you for mentioning the (mental) cost of abstractions!

I generally use both styles where I feel they make reading the code easier, and I'm not married to any one style, but some pointers:
- complex (/overly generic) declarative statements should still be abstracted with smaller, better named, methods
- declarative style becomes much more readable when the language supports piping functions instead of nesting them

But as you allude, I think, the bigger the picture the more declarative style shines. If the purpose of the software is to present something of a domain language to the user, considering a declarative style can be helpful.

defeqel
Автор

Your comment on reducing cognitive load rang completely true with me. I've found that reducing cognitive load becomes more important when a coder uses multiple programming languages. I used to write Perl only, probably one of the most concisely typed programing languages ever known. The same example in Perl would take only a fraction of the lines compared to writing for other programing languages. After stints in Python, Ruby, Go and then repeat when changing jobs, I found with language specific idioms, I would have to relookup language specific idioms every time I switched between languages (even in the same job). It wasn't an issue when I only wrote Perl and quite frankly it was much quicker to get code out when only using one language. My favourite language now is Go which is all about reducing the cognitive load, however there are certain times in which I wish certain features of Ruby or Perl where in Go because it would reduce the extensively longer amount of code I'm writing in Go which I could do in one of two lines of code, an example of this is error handling. I do agree that reducing cognitive load is very important in writing maintainable code, but I wish that concise syntax would not be traded off for readability (cognitive load) every time. Surely there is a way to have both? I hope that language developers would find ways or alternatives that make idioms such as ternary operators, conditional assignment operators, concise and readable. That would be coding nervana.

dexterplameras
Автор

I am usually attracted to something like this:

def sweet_words(dictionary):
def is_ok(W):
return len(W) == 5
return list(filter(is_ok, dictionary))

Still pretty declarative without stacking the Jenga tower of composition too high. And separates the overall point of the function (filtering) from definiton of the criterion (five-letter-er).

AloisMahdal
Автор

The problem with python examples is that sometimes its easy to forget that Python has built-in functions which are in fact, C functions with bindings.
Taking advantage of built-ins in Python is a smart thing, given that they will run faster than any byte-code function implemented in Python. As so, the imperative function will most likely have a penalty performance.

It's not just about the clarity. It's also about the developer knowing the tool so that it takes advantage of its features when it is required. Clarity can have a performance cost which will directly translate in higher operational costs.

ClaymorePT
Автор

I like to think that Imperative vs Declarative Programming are products of Inductive Reasoning vs Deductive Reasoning respectively. In Inductive Reasoning, the thinker starts from specific premises based on observations in order to produce a general conclusion. It’s starting from the complex and trying to make it simpler by generalizing things. A bottom-up approach.

In Deductive Reasoning, the thinker starts from general premises based on theoretical approaches in order to produce an specific conclusion. It’s starting from the simpler, understanding it in-depth, and then composing their next reasoning with its previous conclusions of that’s “true and accurate”, which creates an end result that’s more complex indeed.

Inductive thinkers are focused on productivity, impact and profitability. It’s as if they tend to enjoy the end result (see something working and solving the problem) more than anything.
Deductive thinkers are focused on being immersed in the process, accuracy and understanding.

Deductive thinkers tend to believe that Inductive Reasoning isn’t logical, since it tends to focus on a reaching a goal instead of understanding the true nature of each premise in-depth (subject). Whereas Inductive thinkers tend to believe that Deductive thinkers aren’t rational, since spending more time on understanding each thing in depth cannot compare to making an impact in the real world (object).

If you notice, our favorite programming paradigms can tell us a lot of our own personality too. Imperative Programmers (Inductive thinkers) sees the world in similar way too. They value and/or understand hierarchy, requirements, deadlines, organization, etc. more than Declarative, which can also make them to feel more Pride. They also tend to be more successful financially, since the “business world” is rooted on Inductive.
Declarative Programmers (Deductive Thinkers) tend to see the world not as being full of objects that should respect their hierarchy and so on, instead they see the world as “collecting components and visualizing their internal potential”. In the Declarative Programmer’s mind, if we can make sure that one small thing/component has been understood and tested to be “accurate”, we store it in or own memory as being “true”. Let’s say another day we find something else that’s “true”, we consider that not only both of them are true but also the product created by those two (without any side effects). So when you say that “Declarative Programming is changing the state because it’s creating a new Array”, remember that’s not how it works in our mind. You’re seeing from the perspective of the end result seen in the computer from the product of enforcement towards the machine (telling it what to do), but as the name says, “Declarative” means that from the perspective of the programmer, once something is done IT IS done. If something is not useful anymore, it shouldn’t be just transformed, instead it should be displaced from their current role (the one we know it is true for that occasion) and then start a new reasoning from there, while using the experience of the past state as measurement for a testing that starts inside our heads even before writing a single line of code. That also tells a lot about “Declarative” personality traits. If we find something new that has potential to be “truer” than the past truth, we get immersed into it until we understand it completely (or their innate potentials). That makes us less efficient, productive and unable to commit to a single thing for much longer. On the other way, Declarative/Deduction is far more accurate logically, since it comes from an impersonal point of view, making the “truth being the truth regardless of our personal relationship with it”.

So in my view (which I could be wrong):

Imperative Programmers: Inductive, Externally Productive, Profitable, more organized, can be too committed (fanboy/cultist-like), though full of generalizations based on their own personal experiences, which makes them efficient but less logically accurate overall.
Declarative Programmers: Deductive, Internally Productive, Knowledgeable, more chaotic, difficulty with commitment, though spend too much time experimenting without producing, which makes them less efficient but more logically accurate overall.

PS: Another thing that just popped out in my mind, is that there’s
this view that “the more imperative and low level one is programming, the closer one is to talk to the same language as the machine”. From my personal point of view (of someone who thinks far more declaratively than imperatively, which could also be a personal bias), that’s the complete opposite. In my head, the more I have power towards something, the less “equal” I am with that thing. In my opinion, “talking the same language” means taking a small piece that has been tested, processed and confirmed by the machine as being accurate, understanding “how it works” and “why it works”, and then considering it as something untouchable that I should only use it to make safe compositions. If there’s a situation in the future where somehow it’s been proven that it’s not accurate OR there’s something new that is more accurate and works better, it’s like it “invalidates” every single thing I’ve ever produced/concluded that was composed with the past “component”. This often leads me to feel this urge to refactor things (like code) done in the past and update it to my current view of what’s true. Like, if I learn something that could bring some improvement to a code I wrote in the past, I feel the responsibility to update it myself, regardless if it’s doing its job successfully. So my view (from a Declarative Perspective) is that this way of handling logic is what means to “talk with the machine with the same language”.

Sorry for the long text but I hope I was able to explain it. It took me ages to realize that people actually processed things in their brain in an imperative/OOP-like way (like Gravity Force, a Scalar quantity) instead of processing each thing individually and just visualizing slots where that thing could be used while considering it might bring a different behavior/impact depending on the situation (like Gravitational Field, a Vector quantity).

lucassacramento
Автор

I think of it as a scale. There is no clear separation between imperative and declarative. In practice I mix the two and choose whatever works better (the priorities being:less chances to introduce bugs, coded faster, runs faster)

HoDx
Автор

Great video. Dave is not only talking about their differences but and particularly pointing out scenarios why declarative one can help more due to it could be more generic then results in better abstraction design. While, imperative way is still valuable for a smaller code section, introducing the flows in a method with clarity.

osisdie
welcome to shbcf.ru