adamhaile / S

S.js - Simple, Clean, Fast Reactive Programming in Javascript

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Better documentation (esp. regarding dependency management)

irisjae opened this issue · comments

As of now, I do so believe that S is probably the most well thought through stream/reactivity library of all. Still, I find the documentation/readme somewhat lacking. I feel that because the way S works is so unique, it needs some more detailed explanation to let people understand how exactly S works, to see that S is not making horses*** claims. One of the points I feel might really need improvement is the part about:

Automatic Dependencies - No manual (un)subscription to change events. Dependencies in S are automatic and exact.

This really does sound too good/magical to be true, especially in this kind of library, if no further explanation is provided. I feel the documentation about:

When an S computation runs, S records what signals it references, thereby creating a live dependency graph of running code. When data changes, S uses that graph to figure out what needs to be updated and in what order.

is insufficient to convince a casual reader that this actually works, its not a crackpot claim. Another part of the reason why I'm asking here is that even I myself am somewhat confused, and want a confirmation from you that my understanding is correct:

  • Certainly, no Javascript program is capable of dissecting other functions dynamically to determine their dependencies from their symbols/source/reflection/etc.
  • Even when a S computation runs, not every expression in the computation is necessarily evaluated, so not all dependencies can be captured
  • However! given that the function in question is sufficiently pure, then all dependencies in the function will either be evaluated on the first run, or be hidden behind a fork (if/while etc.)
  • If the condition of the fork is also a S node, then any computations hidden behind the fork would be reevaluated whenever the fork condition is also updated (ie whenever the code in the fork might possibly be evaluated)
  • Thus, automatic dependency detection actually works (lazily)!!

If I am correct, I think it is important for the reader to understand the (reasonable) assumptions S has put on the code given to S computations, and how it actually works. If there was anything I could help with, I'd be glad to as well!

Yep, you've got that correct. It may be helpful to think that computations don't have dependencies, the values of computations have dependencies. Even more specific is to say that the values of computations have invalidators. If any of those invalidators changes, then the value is now invalid, and the computation needs to re-run to produce a new value with its own (potentially different) set of invalidators.

S is perfectly happy with code like:

let a = S.data(true),
    b = S(() => a() ? 1 : c()),
    c = S(() => a() ? b() : 2);

That looks like a circular dependency, but if we think in terms of values, it's not. There are two possible scenarios:

  1. when a() === true:
    b() === 1 with invalidators [a]
    c() === 1 with invalidators [a, b]
  2. when a() === false:
    b() === 2 with invalidators [a, c]
    c() === 2 with invalidators [a]

(Anybody who wrote such code other than to prove a point should be shot, of course 😄 ).

I've got a series of three articles in mind to try and explain more of the reasoning behind S. Standard API documentation doesn't really cut it, because it's not just what the functions are and what their signatures are, it's how they work together.

When I get that together, would you be willing to give it a read and give feedback?

@adamhaile Thanks for your through and quick reply! Sorry I wasn't able to reply, got caught straight in the middle of some projects. Yes, I would love to give you feedback! I appreciate your work a lot.

@adamhaile Any idea when your articles might be published?

Please define exactly(maybe more verbose) the difference between S.freeze() and S.subclock(). Looks literally same in README

run to completion

If you look at the tests, you can see that freeze literally freezes the time during the closure, and flow resumes after the closure, wheras subclock creates a new time context, where multiple iterations in the inner clock are completed within the same instant from the outside. But yes, I totally agree the documentation kinda sucks here as well. I scratched my head around this for a while as well.

By the way (off topic), I stumbled upon this library today, that actually seems to do something similar to S.

the documentation kinda sucks

Yep, ferreal. My priority has been getting Surplus 0.5 out the door, which happened yesterday. Working on a much longer README at the moment.

By the way (off topic), I stumbled upon this library today [1], that actually seems to do something similar to S.

I found cellx [2] in the past, also looks similar

[1] https://github.com/ds300/derivablejs
[2] https://github.com/Riim/cellx

Just pushed up a large rough draft expansion to the README. All feedback welcome. I want to add a few break out sections on core concepts, like what it means to program on a unified global timeline of immutable instants, probably something more about computation lifecycles too. But this provides a lot more discussion about the API.

BTW, thanks for the pointers to similar projects -- as you might guess I like looking at how others have approached the same/similar problem. At a quick glance, derivableJS and cellx look a little more simplistic than S (but hey, obviously I'm biased :)). They don't appear to be glitchless or have any strategy for automatic computation disposal or idempotent side-effects. They look like the same reactive semantics as, say, Knockout.js. I might be wrong though, looking forward to looking closer. If nothing else, I'd like to bench them against S, since they both claim speed as a virtue.

Thanks @adamhaile for taking the time and effort to document. BTW I think your copyright notice is wrong, should at least show the first year of publication [1], which seems to be 2013 if I look through the git history. I would mark it as "© 2013-present ..."

I am still looking forward to your answer to adamhaile/surplus#57 (improve quantification of advantages).

[1] https://www.copyrightlaws.com/copyright-notice-year/

@adamhaile Looks like derivableJS had existing benchmarks comparing itself favorably to MobX. Someone added S.js implementations here: ds300/derivablejs#119

S.js at the top of 2, derivableJS at the top of 3. I have no idea how meaningful these benchmarks are. I'd be interested to hear your opinion, Adam. What would be a meaningful benchmark?

Results here:

{
  "all_pairs": {
    "djs": {
      "hz": 23.329235170835332,
      "rme": 5.139300279358734,
      "samples": 42
    },
    "mobx": {
      "hz": 20.161756029674617,
      "rme": 9.519040687233975,
      "samples": 40
    },
    "s": {
      "hz": 12.949194746968802,
      "rme": 3.9628235271513734,
      "samples": 36
    }
  },
  "fan_out": {
    "djs": {
      "hz": 2.9515524020309365,
      "rme": 9.007185136851865,
      "samples": 12
    },
    "mobx": {
      "hz": 4.480957366220161,
      "rme": 5.526119661739351,
      "samples": 16
    },
    "s": {
      "hz": 32.30267526850888,
      "rme": 3.849298806201103,
      "samples": 44
    }
  },
  "many_atoms": {
    "djs": {
      "hz": 39.551516326685906,
      "rme": 1.5696587864441331,
      "samples": 53
    },
    "mobx": {
      "hz": 26.297220477710145,
      "rme": 1.9636966411820969,
      "samples": 47
    },
    "s": {
      "hz": 21.347110432908906,
      "rme": 2.2993178158955847,
      "samples": 39
    }
  },
  "single_atom": {
    "djs": {
      "hz": 104.78059804383238,
      "rme": 0.5194058791225616,
      "samples": 76
    },
    "mobx": {
      "hz": 4.717643019338226,
      "rme": 2.498165318762352,
      "samples": 16
    },
    "s": {
      "hz": 76.57459554005035,
      "rme": 2.172949230203329,
      "samples": 65
    }
  },
  "waterfall": {
    "djs": {
      "hz": 10.619811066319105,
      "rme": 4.673745467714663,
      "samples": 31
    },
    "mobx": {
      "hz": 7.086785079649039,
      "rme": 4.3536772605938925,
      "samples": 22
    },
    "s": {
      "hz": 52.6638492691589,
      "rme": 3.6274318664631218,
      "samples": 67
    }
  }
}