Declarative Programming: Is It a Real Thing? (2015)
toptal.com
toptal.com
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 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.
- [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.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.
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.
It's domain-specific, but within its domain, it works quite well.
Edit: Apparently functional programming is declarative.
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.
Rosetta Code has "apply a callback to an array" example with Elixir. https://rosettacode.org/wiki/Apply_a_callback_to_an_array#El...
Enum.map([1, 2, 3], fn(n) -> n * 2 end)
Enum.map [1, 2, 3], &(&1 * 2)The database engine will transform your declared goal into an execution plan (query plan), which defines how it will achieve what you want.
Long story short, an orange isn't a weapon in general, but you can make it into one in specific cases.
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."
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.)
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.
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.
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.
The dsDSL is actually pretty close to how React or some functional languages work with HTML.
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.
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 disagree. See https://www.red-lang.org/p/about.html as an example of a language that is purely imperative functional.
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.
* https://en.wikipedia.org/wiki/Hoare_logic
* https://en.wikipedia.org/wiki/Imperative_programming
* https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexic...
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.
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.