Show HN: Leporello.js – interactive functional programming IDE for JavaScript (leporello.tech)
It records an IO trace of your program, which is then transparently replayed during subsequent program executions. This allows you to instantly reexecute your code after making small tweaks, thereby tightening your feedback loop.
Furthermore, Leporello.js can serve as an interactive notebook. You have the flexibility to utilize any JavaScript libraries to visualize your data directly within your code.
For a more detailed walkthrough, please watch the product video. Currently, Leporello.js is available as a free online application that you can try right in your browser. My goal is to build the Leporello.js standalone Electron app and a VSCode plugin, both with TypeScript support. Additionally, I plan to add Node.js support (currently, Leporello.js is only for HTML5 apps). In the VSCode plugin, Leporello.js will sit on top of the built-in TypeScript/JavaScript mode, utilizing its code analysis information to enhance the default VSCode experience with unique Leporello.js features.
I am building Leporello.js as a single independent developer. Leporello.js is funded solely by donations. Support me on Github Sponsors [0] and be the first to gain access to the Leporello.js Visual Studio Code plugin with TypeScript support.
I'll be delighted to answer any questions you may have.
34 comments
[ 3.5 ms ] story [ 220 ms ] threadEdit: that vscode extension with typescript support actually seems perfect for me!
That VS Code plugin sounds particularly amazing. How do you plan to scope things? Notably how would I avoid it accidentally re-running side effects?
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:
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
As a side note who / what narrates the video? ;)
And she pronounced 'console' wrong. The noun should be pronounced /ˈkɑːn.soʊl/, not /kənˈsoʊl/.
Exploring the development of a two-mode debugger that can seamlessly switch between traditional imperative debugging and Leporello.js-style debugging for pure functions in modern multiparadigm languages is indeed an intriguing research avenue.
What’s the functional subset of JS? Any literature on this subset? I don’t really know JS apart from hacking together a few scripts for fun.
If this is purely functional, does this mean we will need to use monads for IO like in Haskell?
In many ways, Leporello.js bears a resemblance to Clojure. Clojure served as a source of inspiration for Leporello.js, to the extent that I even contemplated building Leporello.js for Clojure first.
[0] https://www.youtube.com/watch?v=-6BsiVyC1kM
fib(6), fib(7) // scratch:9:6 - unexpected token
This was a rotten era. Frankly I’m glad the hype is over and people have worn thin on it. Only one more “functional is the best” cycle till I kick the bucket, thankfully.
.map is not strictly functional programming. Replacing your data structures with slow and balls, hardware ignoring garbage because “it’s easier to reason about” while simultaneously producing the shittiest of dogshit software in history is functional programming right now.
```js
monaco.editor.create(document.getElementById('container'), {
});```
When I highlight "hello world" and press ', I want to see "'hello world'" instead of "'"
All that aside, awesome project
I do believe that jsonnet (despite all it's real problems) is an underestimated jewel mostly because of the embryonic level of its tooling. An ide/debugger like this could help.
EDIT: some tangentially related work: https://github.com/kubecfg/ursonnet
In fact, I believe it's possible to write code in TypeScript that would be very similar to Jsonnet. TypeScript provides the capability to create compiler plugins, which can restrict the allowed language subset, effectively making it more limited and suitable for configuration
Evaluation is driven "from the output"; by which I mean that only the code that has an effect on the output is evaluated. When an object is rendered (manifested) all it's non-hidfen fields are evaluated and every expression they contain produces a value which is then manifested and recursively only the fields that are manifested have their expressions evaluated.
This ia particularly important when combined the key operation that jsonnet provides: object overlay/merges. When you overlay an object on another object it may shadow a field of the "super" object causing the expression of the super object to not be evaluated.
- TypeScript gained traction partially because you could migrate an existing codebase to it. Does leporello support gradual migration from style that's not aligned with leporello to the supported subset? That is, how leporello handles a codebase that's partially written in freestyle JS and not the subset that leporello specify handles?
- How does it deal with patterns from popular frameworks or libraries that are borderline frameworks. React, Vue, Express, NestJS etc.
- It feels like leporello has to store a lot of possible branch-outs of the state. How would it handle frameworks code in this case? React on its own probably has a lot of branch-outs inside. Would it devour my RAM?
Leporello is based on the idea that you can have much more powerful dev experience (time-travel debugging, better debugger UX) if you adhere to functional code. Look at the recent news from Jetbrains [0]. They presented predictive debugger for their .NET IDE. They called it "a game changing look into the future" and it is actually a huge improvement of ergonomics. But with FP, you get predictive debugger for free. It is just a trivial consequence of not having data mutations. So inside the function, each name is binded to single immutable value.
If you have freestyle JS, then Leporello is not a good fit for you. It requires some buy-in from a developer. The good thing is that you still write plain vanilla javascript, without non-standart extensions that require some kind of transpilation.
[0] https://news.ycombinator.com/item?id=36940937
React is a great fit for Leporello.js. There is an example of React TODO app that you can write and debug in Leporello. It is showcased in the video. You can play with it yourself if follow the link https://app.leporello.tech/?example=todos-preact
Speaking about backend frameworks, basically you code is a function Request -> Response. How do you organize your code is up to you. You can code it in a functional manner. What is great about Leporello, is that it remembers all the calls your app made to databases, other microservices and external APIs and allows to debug them in a time-travel manner, seeing requests and responses. You can run your code once, and then debug and navigate it forward and backward, seeing runtime values that were generated when your code was executed. It a huge time saving, especially when external resources are slow and may require complex setup or teardown before or after being called.
>> It feels like leporello has to store a lot of possible branch-outs of the state
Could you please clarify, what do you mean by branch-outs of the state?