Live data from Hacker News

The unreasonable effectiveness of declarative programming

bollu.github.io

91–100 of 128 posts

Re: The unreasonable effectiveness of declarative programming

#91
post #52

For me, there are only two benefits to declarative programming: Safety and quick Understandability of code. I would say that markup languages fall into this category (HTML, Markdown etc.) Everything else is better to be done by some sort of imperative language. The declarative stuff can be a subset and a convention but you can always break out of it.

I see it almost the opposite way: now that the machines are so powerful you should prefer the higher-level declarative languages (like Prolog!) unless and until you need the efficiency that imperative languages can unlock.

Re: The unreasonable effectiveness of declarative programming

#92
post #22

The unreasonable effectiveness for toy examples to ignore real world complexity.

A pity the author didn't provide you with a more professionally polished version you could add to your github and call your own.

Two snarks do not make a witticism.

Re: The unreasonable effectiveness of declarative programming

#93
post #70

Earlier quoted context omitted.

Yes. That is imperative. Declarative code describes the _output_ or final desired state of something.

Is f(g(x)) imperative? There's an ordering... and it works in terms of mappings, not outputs...

Depends on what language you're using. In math notation, given `y = x * x`, you can work backwards from `y = 4` to figure out the value of x, whereas in, say, Javascript, `y = x * x` means exactly "compute y as the value of x times itself" and only that. For illustration, we could also compute the square of x in a different imperative form, e.g. in terms of a loop over additions.

Similarly, in mathematical notation, `f(g(x))` can be a way of expressing the existence of some sort of law, e.g. maybe f and g are commutative. That means that if code were written as such in a 5th-gen language[1], the underlying engine is free to recompile the code into `g(f(x))` assuming the commutative property holds and the performance is better. By contrast, in a imperative language, `f(g(x))` generally would compile to that exact order of operations (unless you have a mythical sufficiently smart compiler)

I can see an argument about JIT compilers being smart in some cases, but the philosophical distinction between imperative and declarative paradigms is that with declarative style, the compiler can transparently swap units of arbitrary complexity. For example, given some CSS rules, a browser engine can decide to paint the screen buffer however it wants, be it top-to-bottom, edge-to-center, layer-over-layer, etc regardless of how the CSS was originally expressed.

[1] https://en.wikipedia.org/wiki/Fifth-generation_programming_l...

Re: The unreasonable effectiveness of declarative programming

#94
post #70
post #68

Earlier quoted context omitted.

So you would call: arr .map(x => x + 2) .filter(x => x % 3) .map(x => other(x)) Imperative?

Yes. That is imperative. Declarative code describes the _output_ or final desired state of something.

It's not so simple.

Compiled programming languages are declarative ways of generating machine code. The source code describes what the output or final state (the executable) should consist of, but not how to construct it (that's in the compiler source).

Is the code "read x; print x + 5" declarative or imperative?

It's declarative because it doesn't specify how to read the number, how to print the result, or how to add numbers. It merely symbolically describes the IO and calculations to be performed.

It's imperative because it specifies in a step-wise fashion reading input, performing a calculation, and outputting the result.

Declarative code is imperative from the perspective of the next layer up in the abstraction stack. Declarative code elides implementation details; the we call the implementation details imperative, because they specify the "how" and not the "what", which is the domain of the higher level.

Under this lens, what can we say about this:

  arr
    .map(x => x + 2)
    .filter(x => x % 3)
    .map(x => other(x))
It's imperative if we understand map() and filter() to be imperative operations. If they're declarative - perfectly possible in C# - then the code is declarative, because `arr` could be quite abstract, and do something much more interesting.

Re: The unreasonable effectiveness of declarative programming

#95
post #67

Looking at the first code sample, I wouldn't call it declarative at all. For me, the defining feature of declarative code, is that it doesn't have a list of actions to be performed one after another. That code sample is such a list of actions, which for me makes it imperative code, meaning "first do this, then do that, then do the other thing." The "list of actions" approach is what makes code complexity grow exponen…

Imperative and declarative seem to me to be a matter of degree and sometimes even syntax. Point(x=1,y=2,z=3) is pretty declarative, but "point, with x set to 1, with y set to 2, with z set to 3" is getting more imperative even though it's really the exact same thing. But the syntax makes our mental model a little different, so yay. From there, it's not to hard to go to "scope, with x set to scope(a), with y set to x+1, with z set to f(x,y)," which s the same as C-style imperative "{x = a; y = x+1; z = f(x, y)}." There's a reasonably smooth continuum between imperative and declarative. As soon as you introduce lambda functions, declarative gets absurdly flexible and can model stateful computation in a surprisingly ergonomic way, so it's not even a twisted pedantic equivalence.

Re: The unreasonable effectiveness of declarative programming

#96
post #93

Earlier quoted context omitted.

Is f(g(x)) imperative? There's an ordering... and it works in terms of mappings, not outputs...

Depends on what language you're using. In math notation, given `y = x * x`, you can work backwards from `y = 4` to figure out the value of x, whereas in, say, Javascript, `y = x * x` means exactly "compute y as the value of x times itself" and only that. For illustration, we could also compute the square of x in a different imperative form, e.g. in terms of a loop over additions. Similarly, in mathematical notation,…

I meant in math.

BTW, pedantry: -2 * -2 = 4 too

Re: The unreasonable effectiveness of declarative programming

#97

In Haskell, one can make more general combinators in the following way: type Anim a = (Duration -> a, Maybe Duration) -- Linear interpolation linear :: Anim Duration linear = (id, Nothing) -- Sequencing seq :: Anim a -> Anim b -> Anim (Either a b) seq (f, Nothing) g = (\t -> Left $ f t, Nothing) seq (f, Just df) (g, dg) = (\t -> if t Anim b -> Anim (a, b) par (f, df) (g, dg) = (\t -> (f t, g t), max df dg) -- Constan…

I am literally too dumb to know what you typed... and then all of the following comments.

It is like you are speaking in Martian or something.

Haskell is effectively impenetrable to me.

Re: The unreasonable effectiveness of declarative programming

#98
post #73

Earlier quoted context omitted.

Thank you for the interesting post. Can you explain why the "A complex animation" code example is declarative? I can see that the names of the functions are declarative in style but the code is a series of function calls that seem to be giving instructions to the javascript interpreter to perform a sequence of operations. For example, you describe the code as follows: 1. anim_const(name, val) to set a constant value…

"Declarative programming" at its core seems to be about separating what the code says from what it does, and trying to put more "magic" in between the saying and the doing. But that's not a binary distinction for all kinds of reasons. What a code "says" is already a bit subjective to start with, but then we get into things like, is this "declarative"? def SayHello(): print("Hello!") SayHello() After all, when I type…

My opinion is of course coloured by my experience with Prolog but the way I understand "declarative programming" describes a style of coding rather than any particular programming language features. Hiding the details of execution is not necessary and the compilers and interpreters of most languages do that anyway.

To give an example of what I mean, here's a typical C function that concatenates two linked lists [1]:

  void concatenate(struct node *a,struct node *b)
  {
      if (a->next == NULL)
          a->next = b;
      else
          concatenate(a->next,b);
  }
And here's the same thing in Prolog:

  append([],Ys,Ys).
  append([H|T],Ys,[H|Zs]):-  
    append(T,Ys,Zs).
append/3 is a Prolog predicate that can be used to concatenate two lists. It's a little difficult to get your head around it the first time you see it but basically one interpretation of it is that concatenating a list Ys and the empty list yields the list Ys; and concatenating the non-empty list [H|T] (for "Head" and "Tail") with a list Ys yields the concatenation of the tail, T, of [H|T], with Ys.

append/3 and concatenate() share the same procedural interpretation: "to concatenate two lists walk through the first list until you find the last element in its tail ("[]" in Prolog, NULL in C) and make that element be the first element of the second list [2]".

The difference is that Prolog's declarative interpretation describes not only concatenation of two lists, but also their difference. Indeed, append/3 can be called in different instantiation modes to perform both tasks:

  % Zs is the concatenation of _Xs and _Ys
  ?- _Xs = [a,b,c], _Ys = [d,e,f], append(_Xs,_Ys,Zs).
  Zs = [a, b, c, d, e, f].

  % Zs \ Xs = Ys
  ?- _Xs = [a,b,c], _Zs = [a,b,c,d,e,f], append(_Xs,Ys,_Zs).
  Ys = [d, e, f].

  % Zs \ Ys = Xs
  ?- _Ys = [d,e,f], _Zs = [a,b,c,d,e,f], append(Xs,_Ys,_Zs).
  Xs = [a, b, c] .

  % Starting a variable with an underscore, like in _Xs, _Ys, _Zs, stops the
  % Swi-Prolog runtime from printing the bindings of the variable and
  % cluttering the screen with values you already know ;)
In short, the definition of append/3 doesn't (just) tell the computer to concatenate two lists to yield a third list, like the C program does. Instead, it describes a relation between three lists that is true under certain conditions, specified by the programmer who wrote append/3. It's this property of describing the shape of data, rather than splitting your code into data and operations on data, that mark code as declarative rather than procedural [3].

________________

[1] Copied from: https://www.codesdope.com/blog/article/concatenating-two-lin...

[2] "Behind the scenes" both Prolog interpreter and C compiler have the same "view" of computer memory and the pointers from an element in a list to the next. However, the point here is not that the Prolog interpreter "hides" this fact. If you squint a bit- it doesn't. [H|T] is the concatenation of H and T and you can very well read it as a pointer from H to the first element of T.

[3] And note of course there are more purely declarative languages than Prolog, like Answer Set Programming. But, I know Prolog and You Can't Teach an Old Dog New Tricks (old dogs know all the tricks :P).

Re: The unreasonable effectiveness of declarative programming

#99

Earlier quoted context omitted.

IMHO in a declarative language your example should be an error. You defined x twice. Intentionally or not, this is confusing, for others and for you in a week when you have to look at the code again. Also, why should the second binding override the first? You introduced a temporal dimension where later lines of code somehow override earlier lines of code. That is not a necessity in a declarative language. It may be h…

>> IMHO in a declarative language your example should be an error. You defined x twice. This would be an error in a language with immutable data structures. Declarative and immutable are not the same thing and there is nothing that says a declarative language must have immutable data structures.

> This would be an error in a language with immutable data structures

It's an error from the point of view of equational thinking; (im)mutabilty is not involved.

The name `x` cannot be 1 and 3 simultaneously. It's either 1 or 3. So in the allegedly declarative program, the programmer first claimed name `x` is bound to the value 1, but later claimed it's 3. That's the error.

This has nothing to do with mutating data structures. Like the sibling comment notes, there is not "first-this-then-that" order of evaluation, so you cannot read this as "first `x` means 1, but later it means 3", because there are no notions of "first" and "later".

Re: The unreasonable effectiveness of declarative programming

#100
As others have pointed out, I wouldn't necessarily call this "declarative programming"--this actually is more what I'd call "literate programming". That's semantics, but I will add that in my opinion, literate programming is way more effective than declarative programming.

Literate programming is just syntactic sugar around functional programming. For example:

    qux(bar(foo,baz),garlply)
...becomes:

    foo.bar(baz).qux(garply)
This is the sort of thing that typically emerges when you have immutable objects, and demonstrates a sort of equivalence between immutable OOP and FP. This is why I don't generally care about functional programming versus object oriented programming debates: they're equivalent if you don't mutate. I'm much more interested in immutability than a slavish loyalty to functions over methods. And I tend to agree with the OP that literate programming is very effective.

What people usually mean when they say "declarative programming" is they want to write a config file in, say, JSON, and have that be their program. But that would mean that the implementer of the language would have to think of every possible way that you could possibly want to configure the program, so they start adding customization points where you can write code in an actual language which is called in certain spots. So now you have to know how to program in a normal language, and know how all the different customization points work. Oh and while you're doing that, you can forget about getting anything helpful like a stack trace, because it was declarative so you don't need to worry about what is calling what, right? So you get things that just fail silently and you don't know why, like:

    class Mail(models.Model):
        ordering = '-received'
        sender = models.EmailField()
        receiver = models.EmailField()
        received = models.DateTimeField()
        body = models.TextField()
This orders by received ascending, even though you clearly are telling it to order by received descending... have fun figuring out why! I'm picking on Django here but it's actually one of the best examples of declarative programming. The problem isn't that they didn't validate for this case: I don't think they could do that reasonably. The problem is that it's not really possible to implement declarative programming in a way that catches all the possible errors of this sort.
Post reply on HN