Hacker News new | ask | show | jobs
by dmitry-vsl 987 days ago
I plan to blog about Leporello.js internals, but now I will try to give you a short answer.

When navigating a call tree, Leporello.js evaluates your functions with tracing enabled, collecting execution information and materializing parts of the call tree in memory. This evaluation is lazy, but if a function invokes an IO operation, the corresponding call tree node is saved eagerly to ensure the function is called only once, avoiding duplicate IO operations.

Handling state mutations is more complex. Leporello.js doesn't inherently know if a function, possibly from a third-party library, will mutate its arguments. Therefore, when using such functions, there's a risk of displaying incorrect data when you navigate and debug your code.

Ideally, a language would color functions based on their purity, allowing smart IDEs to deep clone arguments before invoking impure functions, enabling time-travel debugging. Maybe I will add comment pragmas to Leporello.js. They would allow to have state-mutating functions as first-class citizens in Leporello.js. Syntactically, they can be just ordinary comments:

  // leporello: mutation
  function sort(arr) {
     arr.sort() // in javascript, sort mutates array in-place
  }

Currently, this responsibility falls on the programmer. If you use argument-mutating functions, consider wrapping them in a pure functional interface. The viable approach is structuring your program with a 'functional core, imperative shell' architecture. This approach, as seen in Leporello.js itself, involves a main codebase that is pure functional, operating on a single immutable data structure describing Leporello.js's state, while the 'shell' part invokes pure functions and applies effects.

The notable example of such architecture is Redux. Redux architecture fits perfectly for apps build with Leporello.js, as you can see in TODO app example https://app.leporello.tech/?example=todos-preact