Purescript on ObservableHQ

Hello there,

Jordan recommended I post this here rather than on the purescript Discord, so here it is!

I’d like the thoughts of the community on some ideas. I’ve been playing for a couple of weeks with using purescript in ObservableHQ (using Try Purescript / Jun Matsushita | Observable) and it’s been really enjoyable (see also my discord announcement). I used to stream for a bit, but that came with quite a lot of overhead, whereas working in a public computational notebook has the same feel of “working in the open” with a much lower barrier to entry. I think also it’s a different paradigm than an IDE and I’m quite interested in thinking about what it would take to take this further than a “scratchpad” for small ideas.

Today

Right now I can interleave my notes with code snippets and work in an editor cell and ask the try.purescript.org compiler to check my work and run the result in the notebook. The purescript JS interop made it quite easy to get there and I feel like I’m only scratching the surface of what’s possible.

As you can see in the try-purescript notebook and the screenshot attached, it’s pretty much an alternative frontend to try purescript with missing features but also some new possibilities. In fact it’s almost easier that with try.purecript.org as you don’t need the TryPureScript module’s helpers to display html, and there’s no complications with the iframe. You can also interop with any JS in your notebook, but also imported modules and other notebooks. Much wow.

Tomorrow

There are a few things that could improve the experience that are more specific to observableHQ:

  • I’m using codemirror with the haskell syntax highlighting, having purescript specific syntax highligting would be nice.
  • There are some bugs with the codemirror integration (tab to indent doesn’t work and focus switching has quirks) and purescript cells don’t feel first class (Cmd-Enter to save and compile would be nice), I reused a localstorage module to persist code in the browser but that’s not as reliable as I’d like.
  • I think it should be possible to save the compiled JS returned by the try purescript API as a file and attach it to the notebook and load it, such that you can reuse observable purescript notebook as javascript modules.

A bit more involved, but I’ve tried this with relative success it’s already possible to run the trypurescript compiler locally and point your notebook to 127.0.0.1 to extend the package set. So with this approach it would also be possible to improve the experience by:

  • running a language server locally and use codemirror’s support for LSP to have all the nice things.
  • maybe making it easy to push button deploy your compiler and language server to some cloud service with your customisations?

At this point though, you’re running all of this extra stuff locally and it feels a lot like a full local development environment, except you’re using a somewhat less capable frontend (ObservableHQ) instead of your own IDE.

Using the public try.purescript.org compiler API, I think is ok (I hope :sweat_smile: ) if done in the spirit of try.purescript.org itself. And should allow more people to try purescript, but I guess that there’s the potential that the compute bill goes up. And beside the cost aspect, this isn’t built to deal with this use case and scale. But what about also deploying a language server with try.purescript.org? It could benefit the main website too if the editor used codemirror and it’s LSP facilities.

Would there be appetite for contributions in this direction? Or would it be best to package this independently for local or other private hosting options?

Sometime Maybe?

Finally I wanted to address some more fundamental limitations which would require more work to be done, and hurt my head when I think about them and where I’d like most to hear feedback about.

I think my main question, after using this approach for a bit, and what’s missing the most is “What is needed so that I could import a purescript module written in one cell/notebook, into purescript code written in another cell/notebook?”

With bonus points to making this reactive (which is really the observableHQ innovation – props to jupyter where this was incubated – together with reusing cells/notebooks Imports | Observable documentation ). But I think this is fairly orthogonal, and also I don’t know yet how welcoming the ObservableHQ folks would be in general to compile to javascript use cases. So let’s leave this aside.

Not our problem

I suppose the path of least resistance is what I was hinting above, which is to consider that this is not a purescript question, and we treat compiled purescript as ESM modules and bye bye types, we interop with purescript code with Foreign. Each cell compiles it’s own Main module, and I try and make this fit nicely with the reactive imports in Observable. In this case we’d still be missing FFI : You can import PS modules in the registry that have foreign code, but right now you can’t ask the try purescript compiler endpoint to deal with your Main.js (Add foreign module and HTML editor · Issue #8 · purescript/trypurescript · GitHub) and we don’t have the foreign import """console.log('yay')""" blah loophole anymore.

But that introduces of course quite a lot of friction, when why I’d like really is to declare a module in a purescript cell and import it in another.

So the 3 other options I can think of right now are:

Compile external code

Could we allow the compiler endpoint (maybe as a flag) to link to code that’s not in the registry, or we extend the concept of registry to enable custom registries and allow to specify that some module’s purescript code is hosted in an Observable notebook’s cell (or as an attachment). Maybe we also allow to compile modules other than Main and compiler API calls allow to say “compile this, together with these other modules as dependencies which are in these cells”.

Link to external modules

Is there a middle ground where already compiled Purescript code is hosted together with its externs.cbor data and we don’t need to recompile modules which already have been compiled. So we’d pass forward to pass something like a module map of externals to the compiler endpoint saying, just map these modules to this module specifier and don’t try to compile it?

Purescript Compiler as a Wasm module

Another tack on this would be to build the purescript compiler (and language server?) as a wasm library. It seems that the GHC Wasm backend has gotten some momentum recently, so it might be feasible?

Thank you for reading up until here. As you can tell, I had a lot to unload, and I appreciate the effort :pray:

Looking forward to hear any and all of your thoughts!

Just a few thoughts…

AFAIK, projects like this are what the public try.purescript.org compiler API are for, so long as it doesn’t prevent others from using it. But, the cost of computing would be something to monitor if this effort gets more traffic on a regular basis.

It sounds like you may want to reimplement the Try PureScript server in PureScript and then extend it however you want. I don’t think Try PureScript is that complicated as I think it largely just calls the purs binary with your code and the files it has locally. Arguably, you could do a lot more than just that.

For now, this should be done independently. AFAIK, we’re currently thin when it comes to available maintenance time on other infrastructure projects due to focusing on higher priorities like the Registry and the Spago rewrite.

You can always FFI any JS code. So, if you compiled the first PS code into JS and then FFI’d that code in the second notebook’s PS code, I think that would work. However, there would be some constraints that need to be met:

  • the FFI does not use any type class constraints
  • ADTs from the first are created via FFI’d functions (e.g. toFoo i = Foo i) and pattern matched via CPS (e.g. foo onFoo onBar = case _ of ...).
  • you use language-purescript-cst and tide-codgen to parse the type signature of the first’s exported functions/values and then generate a corresponding and usable type signature in the second’s FFI’s value (e.g. foreign import functionFromFirst :: GeneratedTypeSignature).

purs doesn’t currently support this workflow. See Building against precompiled dependencies · Issue #2477 · purescript/purescript · GitHub

I’m not tracking the state of that effort, nor do I know of any other core contributor.