46 comments

[ 3.1 ms ] story [ 82.9 ms ] thread
Instead of talking about declarative programming we should talk about denotational programming - writing programs that map to a consistent mental model.

The fact that something is declarative doesn't mean that it's understandable. Webpack for instance is a piece of technology I consistently struggle to understand and predict the behaviour of, even though it's declarative. (Not intended to shit on webpack's developers / maintainers in anyway.)

I am not a huge fan of the declarative model for much of the same reasons I am not a fan of OOP. Both concepts are full of convention and ritual to decorate code in a way that is easy to understand in isolation and convenient to put together. That sounds great except rarely does anything actually work in isolation and the word complex literally means put together.

Personally I am a big fan of functional imperative. Write instructions to solve the problem as directly as possible with the fewest decisions/statements as possible. Isolate the solution into a function that is referenced when needed. Opposite of complex.

I think you're confusing things. (Pure) functional programming is a form of declarative programming. The most important feature of declarative programming is the elimination of side effects and the idea of referential transparency. It has nothing to do with OOP.
> I think you're confusing things. (Pure) functional programming is a form of declarative programming.

I disagree. See https://www.red-lang.org/p/about.html as an example of a language that is purely imperative functional.

Imperative and functional are opposites. There is no such thing as a purely functional imperative language.
I provided you established evidence to the contrary.
imperative programming is control flow centric. do this, do that, do this other ten times.

functional programming is about referential transparency - the value of an expression is determined entirely by its arguments and not some hidden state.

so you certainly can have imperative functional programming which is both recipe-like and referentally transparent.

declarative programming is the antipode of imperative programming. yes, its referentally transparent, but it also attempts to describe the nature of the solution, rather than the (imperative) process to get there. i dont know that there is a declarative variant that makes sense without transparency...because if there is hidden state and we have no handle on order of evaluation, its kind of a lost cause.

The usual definitions of imperative vs functional are that imperative is built from Hoare triples and functional is built from lambda calculus. By those definitions, you cannot have a functional, imperative language.
When used properly the nested scope provided by lambda calculus allows shared variables accessible from outside a local function thereby accounting for the precondition of Hoare's triples without it being locally present or passed in. This is enough to satisfy both the functional nature of portablity/isolation and the imperative model by Hoare's logic.

* https://en.wikipedia.org/wiki/Hoare_logic

* https://en.wikipedia.org/wiki/Imperative_programming

* https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexic...

> When used properly the nested scope provided by lambda calculus allows shared variables accessible from outside a local function thereby accounting for the precondition of Hoare's triples without it being locally present or passed in.

Yes, you can express any Turing complete system in terms of any other Turing complete system.

> This is enough to satisfy both the functional nature of portablity/isolation and the imperative model by Hoare's logic.

I think you're missing the point. It's not about portability or isolation. It's about the mathematical system you use as the foundation of your semantics. Yes, you can implement a lambda calculus in turns of Hoare triples and you can implement Hoare triples in terms of lambda calculus. Or in terms of unification over Horn clauses or combinatory logic.

What makes a language functional is that its semantics are defined as an extension of the lambda calculus. What makes a language logical is that its semantics are defined as an extension of unification over Horn clauses. All the benefits people suggest to using one style or another are theorems, not definitions.

> It's about the mathematical system you use as the foundation of your semantics.

I think you are conflating lexical scope with functional programming. Lexical scope applies even without functions, but functional programming requires functions. The difference between a scoped block and function is that functions have identity and the scoped block does not even though both benefit fully from lexical scope.

What makes a language functional is whether components are composable via function. That could mean several different things, such as: lexical scope, first class citizens, recursion by reference, callbacks, and so forth. A simple way to thinking about it is things are well separated, sometimes atomically and sometimes in a taxonomical fashion, but that separation has identity. That is very different than an OOP model where things are extended and put together and that togetherness or extension has no identity.

As a side note, everyone here likely knows the most successful declarative language, SQL.

It's domain-specific, but within its domain, it works quite well.

Is SQL a declarative language? It requires predicates for everything. Even declaring a schema requires a predicate, which doesn't seem any different than function arguments to me. "select" abstracting the underlying DB engine's "control flow" doesn't seem characteristically declarative. It seems like the same type of standard library that any imperative language would offer for convenience.

Edit: Apparently functional programming is declarative.

If it's not, then nothing is.
Apparently functional programming is considered a form of declarative programming. That doesn't make much sense to me, but I guess relative to writing assembly or C it is declarative. The distinction seems arbitrary, because even C and assembly contain abstractions for more complex operations taking place in the OS and in the hardware.
(comment deleted)
The difference is referential transparency: In declarative programming, an expression always returns the same result, no matter where and in which order you use it. Contrast this with an expression like a += 1; in C, which returns a different result each time.
A cliche example is "squares of values".

imperative code would be something like:

  fn squares(array A)
    array B;
    for (idx, x) in A {
      B[idx] = x * x
    }
    return B;
  }
(or "B.add" or some other mutating action). whereas 'declarative' code 'declares' what it wants:

  fn squares(array A) {
    return A.map { |x| x * x }
  }

I think "declarative" is used more in terms of "declare the result you want, not how you'll get it". It's easier to focus on what the output of the program is with declarative code.

Higher-level abstractions are probably necessary, but I don't think it's fundamental to how I'd use the term. :-) Although I wouldn't be surprised to see a variety of ways people use the term.

It is declarative. The db does th query planning, not you. E.g. you don't tell the db whether to do a full row scan or an index scan.
Yes, as the predicate defines what you want it to do, not how.

The database engine will transform your declared goal into an execution plan (query plan), which defines how it will achieve what you want.

You can't just blankly state that functional programming is or isn't declarative. sort([42,7,2]) is declarative in that it declares what is wanted not how to do it. But if you write a functional bubble-sort, then that function is not a declaration of intent its a functional reimplementation of a method. Now naturally if your write a Haskel SQL parser/query planner and executor, then you can have a functional implementation of a program that executes a declarative language. And you could easily make a subset of haskel commands that declare your query in a functional manor then, instead of parsing SQL, and that subset, or DSL would be a declarative query language. That does not however make Haskel a declarative language overall.

Long story short, an orange isn't a weapon in general, but you can make it into one in specific cases.

When I think of a declarative DSL, I think of something like cloudformation or HTML. The main advantage of a declarative language is that you can describe a desired end state without being overly concerned about the implementation details of arriving there.

I think the sorts of dsl's that he's complaining about are more hybrid approaches and not fully declarative. I think Chef is an example of a DSL that does that right, in that if you have the right plugins and helpers you can do it fully declaratively, but it's fairly trivial to add in blocks of fully imperative code or define new blocks.

(comment deleted)
An interesting observation in the article is the data structures DSL that the author goes into quite a bit of detail, which is essentially what most Clojure/Lisp developers also talk about.

Basically the author points out a lot of benefits of using the native serialisable data structures of the language itself to model the domain, instead of creating a separate representation which has to be parsed, optimised, evaluated, etc. Lisp users just take the same idea one step further by having the "host" language also be represented using the same data structures, instead of just the DSL's.

As Alan Perlis said, "It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures."

Well, since the author asks so nicely, let me answer. I do not consider that code to be elegant.

Just count how many possible errors you can make on a simple templating DSL, and then count how many errors you can make on that dsDSL one. One is clearly better than the other.

I would say that all the problems enumerated there come from a bad selection of DSLs to study. It's perfectly understandable that the author chooses templating languages, and I don't know of any better templating DSL to put there, but that only evidences that we lack an actually good templating DSL (outside of the scope of Tex). That just shows that text templating is hard, not that DSLs are inherently problematic.

For evidence that the problem is not with the concept of DSLs itself, just try to write the same article using SQL as an example. You can't.

CSS seems to be a real thing ...
Red language is, to an extent, a declarative programming language - especially for the UI.
The article makes a valid argument against bad DSLs. But the dsDSL is also declarative so it's not really an argument against declarative programming. Similarly using a native data structure for a SQL query doesn't make the query any less declarative (and can have a lot of advantages).

The dsDSL is actually pretty close to how React or some functional languages work with HTML.

"Declarative" programming has become something of a buzzword. This is unfortunate because it prevents finer distinctions between domain specific language techniques.

I think it achieved its buzzword status because it for a long time was something people clearly recognized but couldn't define. Fortunately, Neel Krishnaswami wrote a very cogent post putting his finger on the nerve.

https://semantic-domain.blogspot.com/2013/07/what-declarativ...

With this definition we can see that (a) the author misses the boat on "declarative" in a concrete way and instead lands on something much closer to DSLs in general and (b) then only uses a narrow set of DSLs to provoke criticism and thus presents a very weak argument.

The powerful core of his argument—the "unfolding" principle—is not something I can claim to understand fully from this article, but it suggests something worrying: that every DSL must transparently resemble its interpretation. This makes it easy for a user of the DSL to recognize how it will be interpreted and thus achieve ease of understanding... but it gets dangerously close to banning abstraction all together!

In my opinion, the true measure of a DSL is the consistency and diversity of semantic interpretations (or "models") that are valid for the DSL and consistent with one another. This measure seems to establish the resilience and leverage a given DSL's topic might have.

Consider how SQL can describe set operations, an abstract search process, a logical predicate, and also a concrete set of searches achieved across physical data stores.

Anyway, to answer the question in the title—something the article neglects to do—I'd strongly suggest that "yes" declarative programming is a real thing, "yes" DSLs are real too, and "no" they do not automatically fall to the problems one can potentially extrapolate from by getting frustrated at templating solutions.

I take the "unfolding" principle to be that DSLs do not permit abstractions of their own language. This one is true for most DSLs, but not all.
I cannot quite decipher Neel’s interpretation. The math syntax on his site isn’t rendering. Can you provide a simple explanation? This is interesting.
Let me first transcript his math. He's giving a judgement for regex semantics `s matches r`.

    - [for all r] <empty> matches r [unconditionally]

    - [for all c] c matches c

    - [for all w, r1, r2]
      IF w matches r1 AND w matches r2 
      THEN w matches (r1 \/ r2)

    - (no rule for "w matches bottom")

    - [for all w, r1, r2] 
      IF there exists w1, w2 such that w = w1 . w2
      AND w1 matches r1 AND w2 matches r2 
      THEN w matches r1 . r2

    - [for all w, r]
      IF there exists w1, w2 such that w = w1 . w2
      AND w1 matches r AND w2 matches r*
      THEN w matches r*
So he notes that in the semantics of regex we have non-trivial "there exists" quantification expressing the "search" over possible breaks in the target string.

This need for an existential to give semantics to a language is his focus. Perhaps the most obvious example then is Hilbert's epsilon, such as what appears in TLA+

(section 6.6) http://lamport.azurewebsites.net/tla/xmxx00-02-09.pdf

This is called CHOOSE in TLA+ and it's a constructive existential quantifier in that

    a / b === CHOOSE c in REAL : a = b * c
results in evaluating a / b to "some" real value c such that the multiplicative condition holds (or some arbitrary value if no such value exists, fwiw).

This shows up in SQL as being essentially exactly what SELECT is doing: `SELECT * FROM Table WHERE Cond` is

    CHOOSE row in Table : Cond(row)
except now CHOOSE returns all matching values as opposed to just "some" matching value.
Re: "Shouldn't an SQL engine not care about if you used IN, EXISTS, or JOIN if it is truly declarative shouldn't it just give you the correct answer in reasonable time if possible by any of the three methods?"

I don't see this as evidence SQL is not declarative. Declarative doesn't mean there must be only one way to do (request) something, and it doesn't mean the optimizer is required to be well-written in practice. Maybe in some RDBMS it does process all 3 fast. That's not a characteristic of SQL the language.

The author sort of hits on the complexity of writing an external DSL. Even good DSL's like SQL have complicated implementations and are usually treated as a black box. Users don't learn them by reading the source code or stepping through an example in a debugger.

This means you need to treat your fellow developers as end users rather than co-maintainers. You need a good spec, plenty of examples, and probably language-specific debugging tools, which have to be taught as well.

No wonder people look for lighter approaches like macros or internal DSL's (which are often just libraries with funny calling syntax). Reusing existing language tools is often the way to go.

In a very twisted sense even C could be described as a declarative language. C code describes the observable effects under the abstract machine model, and the compiler tries its best to generate machine code that matches those effects. But more importantly the compiler is completely free to ignore the exact "imperatives" that the programmer provides, as long as the defined outcome is the same.
Re: Arguably the most successful declarative programming tool is the relational database (RDB).

I used to use database systems that were mostly imperative. While they do put more onus on the app developer to manage "machine efficiency", such queries were easier to build and study (debug) incrementally, including the machine run-time profile.

Imperative gives you a better chance at fractal divide-and-conquer of tasks and sub-tasks that can be tested and analyzed as independent units. SQL has made that relatively difficult. Views and the newer WITH clause help, but they are still not fractally dissectable within themselves.

There are pro's and con's to each, but I'd like to see the world forked around 1982 into SQL-World and Imperative-DB-World and see which actually does better.

The theoretical benefits of declarative queries being more machine optimize-able is exaggerated when put to practice. If it were true, we wouldn't need DBA's to rewrite slow SQL. Imperative can also be machine optimized.

Declarative can be quite useful for GUI's but only if "events" or equivalent can imperatively tweak the results along the way "between" steps. Declarative may do 80% of the work, but certain operations will need custom tweaking, usually with imperative code.

Optional events added to declarative systems to adjust stuff along the way is a good mix of imperative and declarative: yin and yang: declarative for the common "grunt work" patterns and attribute management, and imperative for custom adjustments. (If the system is designed to integrate them well.)

Not this again. Haskell, Prolog, CSS, SQL are classic examples of declarative programming, but really, what isn't? Everything except machine code is to a certain extent declarative.

Obviously, even the C compiler is free to optimize and rewrite your code to a large extent. Maybe it will put everything in registers! Maybe it will shuffle variables out of memory! Maybe it will skip entire instructions, or unroll loops! Maybe it will completely eliminate code that it believes to be dead! Maybe it will use SIMD instructions to do four multiplications at once! Will it will pad your structs, or change byte order? Will it use 16, 32, or 64 bits for you integers? There's no way to know! Even if you figure it out for your own machine today, the code you write today may be compiled to target a different architecture in the future. Obviously, then, C is a declarative language because the programmer only provided a high-level outline of what he or she wanted and let the compiler figure out the details.

Even at run time, we find that many system calls are declarative for all intents and purposes. What happens when you call malloc()? Well, you tell it what you want, and a memory manager decides how to give it to you using a variety of memory management strategies: maybe they call mmap() or VirtualAlloc() or some other syscall, or maybe they just give you a pointer into a region of memory they allocated earlier and have been hanging onto.

Is even machine code fully imperative? Maybe once upon a time it was, but now with all these caches and pipelines and branch prediction, it seems like even the hardware is making decisions on our behalf about how to "best" run a program.

The way I see it, we can either acknowledge that these computer thingies are in fact to a large extend "doing what we mean" rather than following our explicit instructions and accept the corollary that we are never quite fully in control, or we can all go back to writing assembly for the 6502.

Laynes Law:

Every debate is over the definition of a word.

Or every debate eventually degenerates into debating the definition of a word.

Or once a debate degenerates into debating the definition of a word, the debate is debatably over.

What do you mean by 'over'?
"Pointless to continue", I would assume. I've been in looong debates that end up being tied to the meaning of a word. Our worldviews often shape the way we use and apply language. For example, how one defines "type" may ultimately pivot on how a given person processes categories in their own mind. I've probably spent a novel's worth of text in online debates about "type".
I believe he was being facetious to cement the point.
Okay, thanks. I missed that one.
It's a developers job to build domain specific languages out of imperative dialect.