
The unreasonable effectiveness of declarative programming - bollu
https://bollu.github.io/mathemagic/declarative/index.html
======
ggambetta
Back in the day when I was making videogames
([http://www.mysterystudio.com](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!

~~~
OliC
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!

~~~
ggambetta
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 :)

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

~~~
ggambetta
Looks like Big Fish Games still sells them:
[https://www.bigfishgames.com/games/2452/the-lost-cases-of-
sh...](https://www.bigfishgames.com/games/2452/the-lost-cases-of-sherlock-
holmes/)

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

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

~~~
jcelerier
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](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.

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

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

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

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

~~~
uryga
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)

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

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

------
cool-RR
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?"

~~~
Trufa
So you would call:

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

Imperative?

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

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

------
rkangel
Interestingly, if you watch the Erlang: The Movie
([https://www.youtube.com/watch?v=BXmOlCy0oBM](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.

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

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

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

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

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

~~~
lmm
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"?

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

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

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

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

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

------
one-punch
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/](https://reanimate.readthedocs.io/en/latest/glue_tut/)

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

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

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

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

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

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

No.

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

~~~
randallsquared
Object destructuring in the parameter list comes _very close_ to supporting
keyword arguments, with only a couple of characters overhead.

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

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

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

------
gitgud
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](https://en.m.wikipedia.org/wiki/Fluent_interface)

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

~~~
protomolecule
>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...](https://en.wikipedia.org/wiki/The_Unreasonable_Effectiveness_of_Mathematics_in_the_Natural_Sciences)

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

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

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

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

------
jzcoder
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](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)

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

Highly recommended watch:

[https://youtu.be/dyiLLdkzzRc](https://youtu.be/dyiLLdkzzRc)

------
gherkinnn
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?

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

~~~
reuben364
Basically auto playing anything for me is bad UX.

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

------
jpxw
s/refrentially/referentially/g

Very cool though!

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

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

~~~
carapace
Two snarks do not make a witticism.

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

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

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

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

