Dear all.
These days I have been drawing the picture of enhancing the way of importing JS value with FFI.
Currently, foreign import declarations always must have corresponding JS implementations in the JS file of the same name. Many times I’ve found this rule leads to a bit of tedious work. It would be nice if we could relax this restriction and be able to give PureScript types to JS values in a more flexible manner. So here is my thoughts:
The semantics of FFI might be classified into three types:
importing values defined in the same project
importing values defined in some node module
importing values defined in global scope.
The first type is, roughly speaking, the direct extension of the current FFI syntax.
How about one can import the value exported from an arbitrarily named JS module with syntax like this:
The main reason I want to relax the rule of naming foreign module is the ability of importing something more exotic via FFI. For instance, one may want to import the value declared in the TypeScript or even WebAssembly:
-- Main.purs
foreign import @local "Main.ts" foo :: Int
foreign import @local "foo.wasm" bar :: String -> Effect Unit
-- Main.ts
export const foo: number = 42;
The second and third type of FFI is somehow different to the current form of FFI in that it does not need the corresponding JS code. How about the syntax like this?
-- Second type FFI: importing value from node package
foreign import @module "node:path" "dirname"
dirname :: String -> String
-- Thrid type FFI: importing value defined in global scope
foreign import @external "setTimeout"
setTimeout :: EffectFn2 (Effect Unit) Int TimeoutId
-- if you want to import a value inside a global module, do like this:
foreign import @external ["Math", "random"]
random :: Effect Number
Of course, the syntax I introduced is just an image and we have more things to consider than I regard, but these days I have had some experiment of customizing the PureScript compiler and the result is satisfying to me.
Any feedback would be appreciated. Thanks!
My general response to this idea is, “No, we shouldn’t do that.”
PureScript is intended to be backend-agnostic. The rule you mentioned above that you find tedious is what makes it easier for other backends to be supported and what makes FFI in general “easy”.
Consider the following aspects:
Once FFI is written in the above format, the corresponding PureScript code is no longer portable to other backends. While the above idea would be fine for some backend-specific code (e.g. Node.js), the general principle isn’t.
Once FFI is written within the PureScript source code file, the tools for that backend (e.g. linter, etc.) can no longer “see” the corresponding code.
This adds unneeded complexity to the compiler. At what point does a backend become prominent enough that the compiler should support it automatically? What should its syntax be? Where do we draw the line between supporting “simple” FFI like export const random = Math.random and something more complex?
FFI tends to be written once and then never touched again. I think the tediousness of this problem can be solved via some other method (e.g. a preprocessor)
PureScript is intended to be backend-agnostic.
Once FFI is written in the above format, the corresponding PureScript code is no longer portable to other backends
It is a significant point of view that I’ve completely overlooked.
Honestly, I blindly regarded PureScript as an AltJS and missed consideration of other backends, so thank you for making me aware of that point.
By the way, although the work I’ve made had no positive impact on the community, I could learn a lot about how the PureScript compiler works, so I’m very comfortable now.
I imagine more backends will appear now that purescript-backend-optimizer has done most of the work for you. The only other issue is writing the FFI bindings for core/contrib. Currently, that’s hard to do because the code is scattered across quite a large number of libraries. Once the Registry gets monorepo support, I think that hill will become easier to cross.