Integrating PureScript into a Legacy JS Codebase
Tooling (w/ Electron)
- compiled & used in an electron app
- purescript is pulled in as an npm dep, and electron-compile has a step to compile PS to JS & move files to where they need to be.
For PS -> JS
Purescript has a Utils module which exports some types and functions, and that becomes a CommonJS module that can be require’d from JS.
Purescript native types map directly to native JS types, and share the same semantics (with minimal limitations), so anything valid in PS is automatically usable in JS.
For JS -> PS
A JS file defines some functions or whatever and exports them CommonJS style, and PureScript can just do a
foreign import jsThing :: Int -> Int. This ties together a pair of JS and PS ‘modules’ which have the same name.
If JS can throw an exception, the ffi-imported function needs to catch it and turn it into an Either value, and return it as a value, since PS is incompatible with raw JS exceptions.
Cost of curried functions
PS functions are curried, so a function taking 3 arguments is compiled to a function which take the first, and returns a function which takes the second, etc., so larger code and more function calls (i.e. deeper stacks). Runtime cost is minimal in practice.
Immutability is enforced by not allowing mutability in the language, not by enforcing it by wrapping objects and arrays and only exposing immutable interfaces. This means it has zero runtime cost, since the code is guaranteed immutable at compile time, and then the resulting JS can just use raw objects/arrays/etc. Also no extra library or bundle bloat (Immutable.js is large)!
Team adoption path
Other compile-to-js langs didn’t quite satisfy requirements. PS did, but team was hesitant. Team lead was a big advocate.
Speaker sat down 1-on-1 to enumerate benefits of static typing.
TypeScript vs. PureScript came to a vote. PS won. Consensus was “if we’re moving off JS, let’s go all the way, and get max power”.
React app. Friction implementing first pass of integrating PS, mostly around frequently switching between PS and JS.
Move to use Pux as a React wrapper for PS. In the Redux store, updates notify Views (as normal), but also notify a Pux layer. Then the Pux layer could send out updates just like Views could. This enabled a cleaner, more incremental translation path.
Second big problem they ran into was around wrangling asynchronous computations that required holding a mutex. The JS attempt to this used an abstraction that worked like lock-aware Promises. The problem was that a nested async
.then() callback had no way to know if an outer callback had already acquired the lock, and if it had, it would deadlock. There was no clear way to address this in JS. In PS, they used
Aff (asynchronous effect types tracked in the type system) and
StateT (a state-tracking type) to rig up a system which guaranteed acquired locks would not be reacquired, and thus prevented deadlocks.
Language transition among devs is rough. It’s not just a surface level difference from JS, and there are a lot of new concepts at once, and purity prohibits a lot of patterns which are possible and common in vanilla JS. Only 1 dev on team is able to do solo PS work, but they want to increase that number with pairing, talks, etc.
Lots of pairing helps. Organizing or joining a local community helps. The ability to peek into the compiled JS output (which is usually clean enough to read and understand) helps a lot. PS is much less scary when you see waht it compiles to.
Things they thought were clean enough to be translated easily were not actually that clean.
Eff/Aff (the effect tracking systems) are intimidating at first, but not bad once you match it up with the simple effects they have on the compiled JS.
Should pick your battles on which things to convert.
Training is a high priority. As projects move from everyone working paired with the PS person and more individual solo, concerns come up on things not well understood.
Tooling is young compared with IDEs, but comparable to JS tooling.