Hacker News new | past | comments | ask | show | jobs | submit login
The unreasonable effectiveness of declarative programming (bollu.github.io)
417 points by bollu on May 19, 2020 | hide | past | favorite | 122 comments



Back in the day when I was making videogames (http://www.mysterystudio.com), I implemented something similar for my framework.

Most of the framework was declarative. There were Sprite objects that had an associated Image and a position (among other things). The onUpdate(dt) method of the "screen" didn't explicitly draw things on the screen, it only updated positions and other attributes of the Sprites, and the renderer would do the rest (back in the days of software rendering, it updated only dirty sections of the screen, etc).

Still, making programmatic animations was difficult, and for videogames you want things to look very nice (smooth movement, easing in and out, transparency effects, that kind of thing). There was a lot of { srcX, srcY, dstX, dstY, time } sets, for every thing that moved.

At some point I had an idea similar to what this blog proposes. I called them SpriteControllers, and they could be "attached" to a Sprite. The engine would call each SC's onUpdate(dt) method on every frame, and the SC would change the position, rotation, alpha, or tint of the Sprite.

By default all the attached SCs would work in parallel, so the next step was to have a SCSequence to run things in series. Also each SC was allowed to declare when it was "done". Some did (like linear motion from A to B), some never finished (like a pulsating glow).

It was a fun development in the sense that it made something that was very difficult to do by hand surprisingly easy, and took our games to the next level in terms of visual quality, just by adding a bunch of very short classes.

Making games was fun in a way. Alright, back to work. These protos aren't going to copy themselves into other protos by themselves!


My friends and I love playing "Murder, She Wrote" on a big TV screen during downtime at LAN parties. We streamed it on Twitch for a while. We're big fans, great work!


OMG, seriously? You made my day <3

Try the two Sherlock games if you can get them somewhere - these have a special place in my heart :)


Do you know where to get your games still? My wife would like them... The reason you are not just putting them on archive.org is because of the contracts you had I guess?

Edit: read your other comment and saw the licensing stuff... Shame about games (and such) that you just cannot keep them for sale forever... But just let them die.


Looks like Big Fish Games still sells them: https://www.bigfishgames.com/games/2452/the-lost-cases-of-sh...

Never thought of putting our first-party games on archive.org, sounds like a great idea, I'll look into it :)


Absolutely! I'll definitely do that!


I've got to ask, for stuff like "Murder, She Wrote", did you license anything? If so was that hard and what was the process like?


For MSW, CSI:NY, Sherlock, and others, we were working with a company in LA called Legacy Interactive. Technically, these are "their" games, we did exclusively all the programming and scripting. They got the licenses for the TV properties, commissioned the art, and wrote the storylines. So unfortunately I can't comment on the licensing process, as I wasn't involved :(


Still really cool =)...


Funny thing, I'm working on a game right now, using react and pixi.js and I implemented something very similar to what you're describing here! It's been fun so far


I wrote this to show off how to write a compact and powerful animation library. It turned out to be a nice case study in declarative programming as I wrote it! I'd love feedback on the API design, website design, and content as I'm trying to actively improve in all of these areas.


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 val to name name.
    2. anim_interpolated(ease, name, val, time) to change to a named value with name 
    name to value val in duration time.
    3. anim1.seq(anim2) to run anim2 once anim1 has completed.
    4. anim1.par(anim2) to run anim2 in parallel with anim1.
    5. anim_delay(time) to do nothing for time time.
All these function calls are described by verbs like "set a ... value", "change to a ... value", "run", "do" etc. The way I understand declarative programming (I do most of my coding in Prolog these days) it means that one declares what is the state of the progam, rather than describing what steps should be taken to change it. But your code seems to be describing steps to change the state of the program. It is "do this, then do that" style of coding.

Do you mean something else by "declarative"? Could you say what you mean by "declarative"?


"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 "SayHello()", I'm not specifying how I want Hello to be Said, just that I want it said. Yet, of course, if this is called "declarative" than the word is stripped of all meaning, and we do clearly want it to mean something.

Personally I'd set the line as being somewhere where by definition of the system, there is a barrier set up and you are no longer allowed to "see into" the implementation because it's going to do things so crazy to your declaration that allowing you to see into it would ruin it. e.g. in SQL when you send in your query the query engine is reserving the right to rewrite it in absolutely crazy ways, and in principle, you have no right to ask about what it actually did as long as you get the "right answer". This is because I then like to segue into how that barrier is somewhat illusory because it's always going to take some concrete actions and you may have need to see into those actions. But that isn't the only place to draw the line, it's just where I find it convenient to draw the line so I can ride a pet hobbyhorse. There isn't really a bright line, even though there is clearly some real difference we are trying to talk about.


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).


Not that I want to disparage as the library looks neat and clear, but is that what declarative means in 2020 ? The code looks like mainly a builder pattern for an animation datastructure.

I've always heard declarative as a sort of synonym to programs defined in terms of equational reasoning such as Lustre for instance - a rule of thumb to separate declarative languages from imperative languages is that in declarative languages

    x := 1 
    y := x + 1
    x := 2
y will be 3 as "y := x + 1" must hold.

e.g. it's not enough to just construct a graph-like datastructure for things to be declarative, the language has to do it itself.

Also, note that the par / seq model (https://en.wikipedia.org/wiki/Series-parallel_graph) is not as powerful as a general dag as things such as the K4 graph cannot be represented in it - even though it may make sense in some animation scenarios.


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 how we humans read text (from top to bottom), but it does not mean the computer has to process it in the same way.


> 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.

c'mon, this is an example in a forum post ! of course it doesn't make sense in real life. real life is

   distance := computeDistanceToCenterOfScreen(cursorPosition)
   text := "the current distance is: " + distance
but that just muddies the waters needlessly

> 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.

yes, some languages work like that, but it's not the point - either you assume that you can update values in your declarative bindings like that, or you have to define your binding in terms of some external behaviour that will change in magic ways, which again would not be very nice to do as a simple forum post example


>> 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.


I think the idea is that declarative statements use "equals" like math uses equals, not as assignment. If you say x = 1 and then later say x = 2, that is a contradiction. Usually if you evaluate each statement in order from top to bottom, that is procedural programming. And if you don't do that, it's hard to assign multiple values to one variable because you need a way for the compiler to know when the variable has each value.


> 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".


there’s a lot of different perspectives on what declarative actually means but in my view it means creating a description of what you want rather than how you want it done. in that sense I think the article qualifies. what you have above is also declarative in the sense that you are describing rules, not how to solve for them. i believe this is kind of declarative is ‘constraint programming’ and also qualifies.


I agree, my go-to example of declarative programming is SQL

you say what you want, and the DB figures out how best to get it.


These are the type of blog posts I miss on HN. It used to be what was all over the homepage. Thanks for taking the time to write it.

One bit of feedback: the sliders are almost impossible to click on with a mobile browser. The drag handle should be much larger. There’s also a horizontal scroll bar that appears partially above the bottom of the page, something is overlapping the main content body.


Which mobile browser? Android Chrome looks nice, draggable sliders, all animations working.


I can’t seem to drag it on iOS safari.


Very cool. I overall very much like both your library, and your presentation.

That said, I found the abbreviations initially confusing. For variable names and symbol/strings, maybe hold off until not the first example? For the functions themselves, can you do a non-abbreviated alias?

I also missed the `par` in `seq((...).par(...))` the first time. And, personally, I think `par([...,...])` would be clearer, and allow for some beneficial constructs - is there a way to allow both?

Particularly, I really admire the time travel aspects; that you can, what that means, and what allows that to happen.

It reminds me a lot of what I've tried to do (not as elegantly, so I might come crib from you!) with the pattern renderer in https://github.com/rangerscience/butterfly


> website design

I like the font used for the code, very readable. But underscoring keywords is superfluous and makes the code listing weird - bold font is enough. Also, the line numbers are jarring, they are too pronounced and they are not important to the reader. Maybe make them gray or put them on the right-hand side. This is a common problem with many blogs. I don't care about the line numbers!

The main text is hard to read - this font, a kind of an hybrid of serifs and tall quasi-monospace characters, is weird and hard to read. Try more usual sans-serif font with shorter-height characters, I think it will be much better both for readability and more distinct from the code listings.

The paragraph symbol is obsolete, I would lose that. At least don't make it look like a link, nothing happens when I click it.


I really like your website design, very clean and readable without sacrificing style.

The library is very nice too. The only thing I would add is, uh, rub some Category Theory on it. It seems like you could "go all the way" and define a formal mathematical semantics for animations, which would be nice. VPRI STEPS project made a quasi-mathematical language called Nile for compositing 2D vector graphics ( http://www.vpri.org/pdf/tr2009016_steps09.pdf ) that might be something to look at.


Small typo:

  On being invoked, it sets out.v = field 
should be:

  (…) out.field = v


I appreciate the simplicity and visual style of the blog.


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 < df 
           then Left $ f t 
           else Right $ g t
        , df + dg)

   -- Parallelism
   par :: Anim a -> Anim b -> Anim (a, b)
   par (f, df) (g, dg) = (\t -> (f t, g t), max df dg)

   -- Constant
   pure :: a -> Anim a
   pure a = (const a, Nothing)

   -- EDIT: forgot delay
   delay :: Duration -> Anim ()
   delay d = (const (), d)
I know there's an Applicative in there, but I'm not sure where else this lies in the typeclass hierarchy.

EDIT: More fixes

EDIT: I should really test in GHCI before I post these things. But I'm lazy.


That looks neat. Making me fall in love with Haskell again which I want to avoid as I can’t use it at work ;)

You could add a monoid instance which would be useful (for one example) if the animation produces a matrix transform then you could combine two animations that produce transforms. For example a rotation and a move. I’m sure there are other good monoid animations. Strings would be a simple example I guess.


This is almost exactly how reanimate generates animations with Haskell: https://github.com/Lemmih/reanimate

Animations are frames over time and can be composed using 'seqA' and 'parA' combinators.


One thing I realized that seq'ing too many things might be bad for performance. As I understand it,

   foldr1 (\x y -> ((x `par` delay 1) `seq` y)) $ map pure [1..]
Would require more and more comparisons over time. Is there some sort of codensity like trick that you can do? Or is this something the user of the library would worry about if it came to that. Or just have a redundant API.


pedantic stuff:

typo here:

  seq (f, Maybe df) (g, dg) t
s/Maybe/Just/

and `par` should be

  par :: Anim a -> Anim b -> Anim (a, b)
  par (f, df) (g, dg) = (\t -> (f t, g t), max <$> df <*> dg)
---

i'm not sure about `par` - if my understanding of Anim is right, it'll take the shorter animation over its specified duration. what if it was something like:

  par a@(_, da) b@(_, db) = (ab, max <$> da <*> db)
    where ab t = (a `at` t, b `at` t)
  
  
  at :: Anim a -> Duration -> a
  at (f, Nothing) t = f t
  -- freeze when t exceeds dur, i.e. the animation is done
  at (f, Just dur) t = f (min dur t)
might be less elegant but i'd expect the combinator to respect an animation's duration.

or is that not what the duration represents? i.e. is this correct:

  stretch :: Float -> Anim a -> Anim a
  stretch factor (f, dur) = (\t -> f (t / factor), (factor *) <$> dur)


Thanks. I should have just clarified that I wanted to communicate the idea. I didn't expect to get it right first try.

Maybe I'll try and make a small library of it if there isn't already. I've never posted something on Hackage.


Replying to your edit:

As I was writing it my headspace was that the duration was for seq to know when to switch. I wasn't really thinking about stopping the animation too.

(I also notices that my seq is wrong. The subsequent animation should start at the beginning, so it should be `g (t - df)`, not `g t`

I believe you are correct. Your implementation is more at the heart of what I wanted to show. . The main point I wanted to get across was the interface itself.


it's a nice idea, i was just in a nitpicky mood :)


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.


I dunno, I prefer to call it too practical. Lets just do terrestrial programming.

Something like this?

  @keyframes ball{
    0%{  left:50px; top:100px;width:0px;  height:0px  }
    10%{ left:50px; top:90px; width:20px; height:20px }
    20%{ left:50px; top:90px; width:20px; height:20px }
    30%{ left:300px;top:50px; width:100px;height:100px}
    50%{ left:300px;top:50px; width:100px;height:100px}
    60%{ left:200px;top:50px; width:100px;height:100px}
    90%{ left:250px;top:100px;width:0px;  height:0px  }
    100%{left:50px; top:100px;width:0px;  height:0px  }
  }
https://jsfiddle.net/gaby_de_wilde/eoj875qr/


I thought so too, but Haskell was an object of adoration and envy for me long enough for me to become hooked.

If Martians visited Earth, surely you'd want to learn their language. :)


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 exponentially, because whenever you look at a series of 10 instructions, you have to think "what state was created by the combination of the first 7 instructions, and does the eighth instruction depend on that state?"


> 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."

Animation at its core is a sequence of images. Imperative animation code would describe how those images change every frame. (For example, a for loop in which you multiply properties by i in order to change them over time).

Declarative animation code would let you define keyframes — how the images look at specific points in time — and the library would generate the images in between them. (This is called "tweening" [0]).

The sample code in this case isn't a list of actions to be performed one after another. It's a list of states (keyframes), which the library interpolates between. That sounds declarative to me!

[0] https://en.wikipedia.org/wiki/Inbetweening


Spot on.


So you would call:

  arr
    .map(x => x + 2)
    .filter(x => x % 3)
    .map(x => other(x))
Imperative?


Compared with SQL it's definitely imperative. It's definitely using higher-level abstractions than a `for` loop, but it still specifies the order of operations.


Actually it doesn't unless you're assuming eager evaluation and that 'arr' is something like an array, rather than something which monadically collects functions for evaluation later.

In C#, the functions being passed could be passed as analyzable syntax trees, and the implementation could actually be in SQL.


nope. Sequencing and imperative have an overlap, but they are not the same thing.

Sequencing is a fundamental computing construct. Data flow graphs allow you to specify sequences, just as shell pipelines do. SQL has sequentiality built-in with nested queries. None of these are imperative environments.


> it still specifies the order of operations

Yes, although not necessarily all of them. Some languages will process this eagerly, some lazily.


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...


I meant in math.

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


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.


Yes and no. It’s a functional chain of higher-order operations over a collection.

So yes—-it is imperative, but it uses functional abstraction to reduce complexity.


The border is fuzzy, so there wouldn't be a yes or no answer. Here are a couple of things that make your example more declarative than OP's code:

1. While each line is a separate step that's done in order, it's done on the results of the previous step. So it's like "get me the sum of the products of the results of foo" rather than "do x, then do y, then do z".

2. Your example has less steps.


I would. Because at the pure end imperative->declarative spectrum, you lose turing completeness.

It is certainly less imperative than the equivalent C, that's for sure.


That's not true at all. The following is a declarative program:

    Yield an interpretation of a set of boolean variables that satisfies the following conditions:
      - A AND B
      - (NOT C) or A or B
      - A AND (NOT C)
      ...etc


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.


It's not really about syntax. It's about the execution model. Your second example might almost feel normal to someone with a preference for SQL, which is a declarative language.

Declarative languages don't specify (or minimize the specification of) the execution, imperative languages specify the execution. You can look at the verbs used in describing or verbalizing the language. In declarative languages you don't talk about "assigning" as much as you talk about relationships: "x is y", "x is related to y by f(x,y)", "if x is predicate(x) then y is z else y is z'". In imperative languages you do things: "x is assigned y", "for x in y do ...", "if x is predicate(x) then do y is assigned z else do y is assigned z'".

Additionally, statements/expressions in declarative languages can be reordered more freely (the "purer" the declarative language the more true this is), given that it tends towards the relational version. In a constraint based system, for example, you could do these in either order:

   x in 1..10
   x % 3 == 2
   ;; => x \in {2, 5, 8}
x collects these constraints and so the order is irrelevant (though practically many declarative languages aren't this pure so the order may matter for various reasons).


Sequencing is a fundamental computing construct, not the exclusive preserve of imperative programming. The 'do' notation in Haskell, function composition operators, shell pipes, data flow graphs are all expressions of explicit sequencing.

Imperative programming is about each statement altering a program's state, not the act of sequencing.


> what state was created by the combination of the first 7 instructions, and does the eighth instruction depend on that state?

You would still have the same problem by using a notation that looks like function application.

The alternative is to repeat all unmodified values, so that looking at each animation step, you know what the output state is.


Can we say the difference between imperative and declarative is ordering or associativity?


Interestingly, if you watch the Erlang: The Movie (https://www.youtube.com/watch?v=BXmOlCy0oBM) they use the term 'declarative programming' to refer to what we would now call 'functional programming'. It's an interesting way of thinking about it - these sort of declarative interfaces are very simple in languages with first class function support.


Prolog was inspiration for the Erlang syntax and it's about as declarative as you can get.

In Prolog, everything is either a fact or a rule involving facts. If you leave some variables unbound, you get what we think of as a program, where the system tries to infer values.


C has first-class functions support, but I don't think it's very easy to define such interfaces in C. Closures and automatic memory management seem to be the "magic dust" that makes this nice to use, first-class functions are necessary but not sufficient.


C doesn't have first-class functions, because you can't define new functions in general places (only at top level).


The definition of first-class functions is the ability to treat functions as data, which C supports. Yes, they are cumbersome to work with, since they must always be defined at top level, but that is just missing syntax sugar. GCC even allows nested function definitions.


> Yes, they are cumbersome to work with, since they must always be defined at top level, but that is just missing syntax sugar.

It's not just syntax sugar. Try writing a function that takes an array of integers and an integer x, and sorts the array mod x by calling qsort. In C this is not just cumbersome but impossible.


We can do it with a global variable.

    static int reg;
    int cmp(int a, int b) {
       return a%reg - b%reg;
    } 
    void qsortModX(int[] a, size_t len, int x) {
      int tmp = reg;
      reg = x;
      qsort(a, len, sizeof(*a), cmp); 
      reg = tmp;
    } 
We could even wrap qsort so that you could pass something much closer to a closure to it :

    static void* g_ctx;
    static int(*g_cmp)(void*, const void*, const void*) 
    int compare(const void* a, const void* b) {
       return g_cmp(g_ctx, a, b) ;
    } 
    void qsort_cls(void*[] a, size_t len, size_t elem, int(*cmp)(void*, const void*, const void*), void* ctx) {
      void* tmp = g_ctx;
      g_ctx = ctx;
      int(*tmp_f)(void*, const void*, const void*) = g_cmp;
      g_cmp = cmp;
      qsort(a, len, sizeof(*a), compare); 
      g_ctx = tmp;
      g_cmp = tmp_f;
    }
With this, you can define a `struct closure` that encapsulates a function and some data and use that to pass to qsort_cls. You can even make it thread safe by using thread local variables instead of globals.

Basically, if you want higher-order functions in C, you can do it, but you need to take a context pointer and a function which accepts a context pointer. The writers of qsort didn't think of that, so we had to resort to global variables to do it, but you could also re-write qsort to avoid the need for the global variable.

As I said, we're missing a lot of syntax sugar, but we can still work with functions as first class objects in pure C.


What you're proposing requires extra work to be threadsafe, is even less typesafe than normal C functions (you've lost checking that `ctx` is actually an `int`), requires you to reimplement it for each standard function you want to use, and is significantly syntactically more cumbersome even after you've done all that. If that's "first class" then how bad would things have to get before you called them "second class"?


I am sympathetic to what you're saying, and in the end this is just a matter of definitions.

I would note though that with C's type system, you always have to choose between type safety and genericity, this is not exclusive to higher order functions. C doesn't have a notion of threads or thread safety, so talking about thread safety in pure C does not make sense. And the fact that the designers of the stdlib didn't think about supporting closures still doesn't mean that the language itself doesn't support them. Other foundational libs, like pthreads, do have support for this style of closures built in.


> with C's type system, you always have to choose between type safety and genericity, this is not exclusive to higher order functions.

True, but functions defined via some kind of "struct closure" scheme are non-typesafe even when monomorphic (e.g. if all the types are int).

> C doesn't have a notion of threads or thread safety, so talking about thread safety in pure C does not make sense.

I'd hold that a function that relies on a global variable is noticeably second-class in a number of ways. Multithreading is one place where this comes up, but you also have to be careful about using it in a recursive context, or use in a library that might be called from more than one place.

> And the fact that the designers of the stdlib didn't think about supporting closures still doesn't mean that the language itself doesn't support them.

Any Turing-complete language "supports" any feature of any other language in a sense, because it's always possible to emulate that other language. If we say functions are first class then we mean not only that it's possible to represent functions as values (because that will always be possible), but that functions represented this way are just as good values as the language's built-in notion of values, and just as good functions as the language's built-in notion of functions.


My understanding agreed with your respondent. I haven't dug deeper but per wikipedia there's a split in the CS community as to whether function literals are necessary for functions to be first class. I think we all agree on the reality of the situation, whichever way we decide that ambiguity.


i think of declarative and functional vs imperative and nonfunctional as kind of the same thing. is there a word for nonfunctional?


Declarative is great, and I wish more people created clean declarative API with regular languages instead of writting yet another DSL.

But remember that the problem with a declarative syntax, is that it needs a runtime, which typically the end user doesn't touch. And if the runtime doesn't take into consideration one use case, the user is stuck. Don't forget to provide an escape hatch.


The other problem with declarative code on a runtime you didn't create is traditional forms of debugging go out the window. You can't set a breakpoint, or single step, or even printf debug declarative code directly, though you can peek behind the scenes a little if you control the runtime.


Yeah, you may debug the result of your declarations if introspection is ok, but god has mercy if the error is at the runtime level.


My impression of declarative programming is that it looks good in the "Hello, world" and other basic examples because you don't have to dip into the escape hatches. But when presented with real-world problems, you end up spending more time trying to get their framework to do what you want through the escape hatches than if you had just written your own program using a normal programming language and a well-designed library.


You can use a normal language to well design a declarative library. That's the point.


You're just throwing terms around here.

Something like Django or Rails, is no longer just a "declarative library" it's a framework. You're using their patterns, and you're limited to their escape hatches.


Never talked about frameworks.

Declarative is just a paradigm, you can use any language to design a declarative API.

Nox, pydantic and sqla are examples of it.


I'm not familiar enough with any of those tools to evaluate what you're saying. :shrug:


Declarative code doesn’t need a runtime if you have a compiler


Even if the language is compiled, you still need a runtime.

GHC compiles to native code, but there's still a runtime for instance to handle the garbage collection, to sort out the lazy evaluation, and to translate the blocking IO API at the haskell end into a non-blocking IO api at the kernel end.

I surely wouldn't want to be debugging my haskell code on a time budget by directly looking at the machine code that is running. I know some people do that and it can be quite illuminating, but the fact that I can debug haskell in its own terms is good.


You're conflating runtime with interpreter. Interpreter translates non-native code to native code, a runtime is what gives you garbage collection, the vm(ex: JVM), std libraries, etc.


I believe this library would be very useful for simple animations. The small size and simple API should make this usable in many cases.

As for more sophisticated animations, I can think of reanimate [1], which outputs animations in SVGs, and should work on the web with wasm if integration is needed [2][3].

I completely agree that declarative programming (and functional-style programming) shines for composing animations. Imperative programming does not show the intent as clearly, and makes it hard to reason about or time travel animations.

[1]: https://reanimate.readthedocs.io/en/latest/glue_tut/

[2]: https://github.com/tweag/asterius

[3]: https://github.com/Lemmih/reanimate


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.


I was expecting an article on the topic of declarative programming. Instead, it's about a JavaScript animation library.


Declarative would be more like:

circle is at position 10,10 at time 1.5

circle is of size 50 at time 5

circle is at position 20,50 at time 3

And so forth. And because of this statements, an animation is computed that respects the declarations.


Unsolicited code review regarding code readability:

If you write a comment saying x is y. Then rename x to be y. The comment adds a layer of abstraction. Now every time you encounter x you refer to the table of abstractions to get to y.

In a similar way the library has shortened function names. This is another layer of abstraction so you need to decompress the shortened function name once again.


If you want to show off how simple your API is then I can think of several things that would improve clarity:

1. Don't use parentheses in comments. They make things more visually confusing when javascript imposes enough syntactic clutter as it is.

2. What's with /* */ ? Doesn't js allow keyword arguments?

3. Your method names are probably a little short for my taste. I'd be happier with sequence() and parallel() instead of seq() and par(). Par especially seems a little too obscure when first encountering it.

4. "cx = location | cr = radius" - why not allow both forms? - or just the clearer one rather than the shorter one.


Thread neighbour gcb0's answer is censored by Hackernews. (Turn on the showdead setting in your profile to see it.) I want to answer, but I can't attach directly to the hidden post, so I have to go up one level.

----

> 2. What's with /* */ ? Doesn't js allow keyword arguments?

It does since version 6. Example:

The function call anim_interpolated(ease, name, val, time) should be rewritten thus:

anim_interpolated({ease, name, val, time})

The corresponding function definition should be rewritten thus:

function anim_interpolated({ease, name, val, time}) { … }


Re: #2, it does not


> 2. What's with /* */ ? Doesn't js allow keyword arguments?

No.


I couldn't remember if that was one of the things they fixed with ES6.

I wonder if passing in a dict is a good alternative in this case. More syntax clutter but at least the params have some semantic meaning.


Object destructuring in the parameter list comes very close to supporting keyword arguments, with only a couple of characters overhead.


Qt with QML does exactly this with animations. It works very well. You don’t really need a functional language to do this kind of thing. I’ve written classes in C++ which are composed to produce different behaviors. Composition in OO is a way to accomplish this without using a functional language.


A great example of the power of [1] fluent API's (aka The Builder Pattern).

Basically, method-chaining allows you to concisely express programs, by hiding irrelevant complexity behind abstractions.

[1] https://en.m.wikipedia.org/wiki/Fluent_interface


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.


> As an example, a staggered animation is a nice way to make the entry of multiple objects feel less monotonous.

Hopefully readers considering this sort of thing for UI transitions will bear in mind: this is nice if the goal of your application is entertainment, deeply irritating if the goal of your application is productivity.


I'm curious about how one is supposed to reason about the time and space complexity of declarative programs. I don't work in a realm where CPU and memory costs can be assumed to be infinite (or even cheap). How do declarative programming paradigms typically offer guarantees or bounds on computation cost?

I'm assuming that any declarative language powerful enough for general use is expressive enough to represent an NP-complete problem; i.e. : Find a set of booleans X1..XN that satisfy Y logical statements (or prove that there is none possible). Therefore absent opening up the "black-box" of the language, the time and space complexity could be unbounded.

With an imperative language, of course, since the programmer is probably specifying the exact order of operations and data structures being used, reasoning about and bounding the cost is usually straightforward.


Quite simple: you don't. Time and space complexity is an implementation detail, if you care about those things then declarative programming is out.


You should look at Actions in the Python implementation of Cocos2d. It uses operator overloading to allow '+' for sequential actions and '|' for parallel actions. These are composable and reversible for quickly creating complex animations.

http://python.cocos2d.org/doc/programming_guide/actions.html

Example:

        bounce = Jump(150, 0, 4, 2)
        scale = ScaleBy(2, duration=1.5)
        rot = RotateBy(360, 0.80)
        scale_and_rot = scale | rot
        bounce_in = bounce | scale_and_rot
        bounce_out = Reverse(bounce_in)

        logo.do(bounce_in + bounce_out)


Your article reminds me a lot of Brandon Kase’s talk regarding Algebriac animations written in Swift

Highly recommended watch:

https://youtu.be/dyiLLdkzzRc


Interesting read. Until safari on my iPhone 7 crashed.

Could it be a memory leak? Or simply too many animations for my ageing phone to handle?


The website uses SVGs for animation instead of the canvas2d API, I'm fairly certain that's the cause.

(honestly I love SVGs for static images or at most one or two animations when used on a web-page, but if you're not using canvas2d or even webgl with a thin shim for anything more complex than that, mobile users will suffer)


For me animations mixed with text is bad UX. Would be better with buttons to start the animations when you have read the relevant paragraph.


Basically auto playing anything for me is bad UX.


s/refrentially/referentially/g

Very cool though!


I don't like weird sounding headlines. Why they keep inventing them?


>I don't like weird sounding headlines. Why they keep inventing them?

It's the opposite -- they are reusing the old ones.

https://en.wikipedia.org/wiki/The_Unreasonable_Effectiveness...


What's wrong with it? Besides maybe not knowing these words


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


You can surely do better than drive-by snark?

Where do you feel this is likely to break down? What would a better approach look like? Is it intractable or can one have both simplicity and power? I suspect the answer is "yes" in which case OP deserves a better response.


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.


In non-declarative way it would look shorter and clearer

anim_set("cx", 100); anim_set("cr", 0); anim_interpolate(ease_cubic, "cr", /val=/10, /time=/3));

etc

don't know what are you trying to achieve here


First off, this part:

>anim_interpolate(ease_cubic, "cr", /val=/10, /time=/3)

is still declarative.

In any your example is incomplete and proves nothing because the end result should be a function that takes t and returns cy and cr. Not sure what your code is supposed to be really.

Secondly, the author explained at length what the advantages of their approach are (purity, composition, time travel debugging, ...).

I'm not sure what you're trying to achieve with your comment except trying to make yourself look smarter by deriding others.


You confuse declarative and functional. Declarative is when the sequence of execution is not related to the order of operators in the program text, but is derived at run time. The animation, where sequence of events is known exactly, is the poorest example for declarative programming.


> You confuse declarative and functional.

No I'm not. Also functional programming is declarative programming (but not the other way around). If your code is supposed to be functional, it is also declarative.

I was trying to freely guess what your incomplete example was supposed to do. Maybe I was wrong, but then you didn't provide much to work with.

> The animation, where sequence of events is known exactly, is the poorest example for declarative programming.

This wasn't a competition to come up with textbook examples of declarative programming.




Consider applying for YC's W25 batch! Applications are open till Nov 12.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: