And once they’re tired of the verbose and impenetrable interop story and comparative lack of tools for abstraction they’ll find PureScript and deliver even safer apps faster.
Uh, but it is a LOT easier to bind externals with bucklescript than it is purescript though? o.O
I beg to differ. I didn’t realize how bad the interop specification in BuckleScript was before I actually sat down and used the PureScript one. Interop and the compilation artifacts are in general better in PureScript across the board. The amount of odd tokens and directives to use is reason alone to take a step back, IMO. It’s probably the worst part of BuckleScript. It’s like a tiny language all on its own; a terrible, opaque one you have to read the docs for every time you’re trying to do something.
Hmm? I’ve not had issues with it? What’s being referenced? They tend to be pretty straight forward. I generally just define a type to ‘encapsulate’ what I’m going to be mapping around and then just
external function calls over those types.
Isn’t the Bucklescript JS interop a super-set of the Purescript JS interop? If you don’t use any of the annotations for using methods, the
new keyword, non-partial application etc it works the same as Purescript. Perhaps I’m misunderstanding something?
This whole dictionary of keywords is the problem. FFI shouldn’t have this many decorators or perhaps any decorators at all. It’s entirely realistic to take someone who’s well versed in writing OCaml and has compiled it via BuckleScript a ton, and they still would have issues understanding what’s going on in a FFI module.
It needs to be scaled down, IMO; just have plumbing functions that do one thing and generate new interface functions that map better to OCaml instead of trying to do it in one go with these opaque decorators.
To illustrate the difference:
getItem :: ∀ m. MonadEffect m => Storage -> String -> m (Either StorageError String) getItem storage key = liftEffect $ runEffectFn6 getItem_ NoStorageError NoValueError Left Right storage key setItem :: ∀ m . MonadEffect m => Storage -> String -> String -> m (Either StorageError Unit) setItem storage key value = liftEffect $ runEffectFn4 setItem_ NoStorageError storage key value foreign import getItem_ :: EffectFn6 StorageError -- error to return on no storage (String -> StorageError) -- no item with key error (StorageError -> Either StorageError String) -- decode error (String -> Either StorageError String) -- constructor to wrap with on success Storage String (Either StorageError String) foreign import setItem_ :: EffectFn4 StorageError -- error to return on no storage Storage String String (Either StorageError Unit)
type t external set_item : t -> string -> string -> unit = "setItem" [@@bs.send] external get_item : t -> string -> string option = "getItem" [@@bs.send] [@@bs.return null_to_opt] external local_storage : t = "localStorage" [@@bs.val] module Make (M : Localstorage_sig.LocalStorable) : (Localstorage_sig.LocalStorageInterface with type value = M.value with type key = M.key) = struct type key = M.key type value = M.value let set key value = let serialized_key = M.serialize_key key in let serialized_value = M.serialize value in set_item local_storage serialized_key serialized_value let get key = let serialized_key = M.serialize_key key in match get_item local_storage serialized_key with | Some string_result -> M.deserialize string_result | None -> None end
Note that the PureScript version is much safer, works for different types of storage, etc., so it’s more fleshed out. The point here is that the FFI in PureScript is entirely done within the normal syntax of the language + bog standard JS files that they’re mapped to, without Perl-level decorators that don’t even fit in. The point here is that everything is just functions and there should be no need to make this sub-language at all.
I don’t understand what prevents the Bucklescript user from replicating the design in the Purescript example (minus the Effect monad). What’s the limitation?
I think you might be misunderstanding something here. The BuckleScript FFI syntax is not something introduced by BuckleScript. It’s an intrinsic part of OCaml (well, two things: OCaml’s
external syntax for binding to foreign values, and extension points for AST transforms). This syntax is pretty well known in the OCaml world. For example, here’s a new library that’s doing something similar to BuckleScript for C bindings: https://discuss.ocaml.org/t/ann-initial-release-of-ppx-cstubs/3708
Regarding your comparison, there are actually several differences. The BuckleScript code is additionally serializing and deserializing the LocalStorage keys and values, something the PureScript code doesn’t seem to do (if I’m reading it right). That whole
Make module can be thrown away without losing the equivalent functionality to what PureScript is doing. Well, it’s true that you have to catch potential exceptions, but it’s always possible to read a documentation comment that mentions that an exception will be thrown, and use try-catch. Or actually even use one of the BuckleScript effect libraries which will wrap things up for you.
Finally, you talk about the ‘whole dictionary of keywords’ in BuckleScript but what about the equivalent new ‘words’ to learn in PureScript? E.g.
,EffectFnN`, etc.? Either way you’d be learning some new things, neither approach will magically work without reading some docs.
That’s actually a fair point. Checking the resources on the website it’s never been presented as an alternative as far as I can tell, and the decorators are emphasized all over the place. I’m not sure if it’s doable, but it’s certainly not idiomatic.
Looking around for examples I can’t find anything that fits into actual OCaml/Reason. The decorators are used everywhere:
[@bs.val] external window : Dom.window = "window"; [@bs.val] external document : Dom.document = "document"; [@bs.val] [@bs.scope "window"] external history : Dom.history = "history"; [@bs.val] [@bs.scope "window"] external location : Dom.location = "location";
No, I think you just highlighted my point for me. These decorators may have a common syntax point in FFI for different compiler targets, but each decorator is part of a domain language that you’ll have to learn for each case where they exist. These particular extensions happen to belong to BS and so you’ll have to learn them, only for BS.
The PureScript code is part of a bigger set of functions in a library (https://github.com/gonzooo/purescript-storable) and yes, I included the functor in the OCaml case, because that’s where we use them.
Here’s the equivalent PureScript code:
class Storable a <= MonadStorage m a where -- | Writes a storable `a` in a monad `m`, returning `Left NoStorageError` representing the lack -- | of a storage on error or returning `Right Unit` on success. store :: a -> m (Either StorageError Unit) -- | Reads a storable `a` when given a key in monad `m`, returning either a `Left` containing -- | an error representing unavailable storage, decoding error or no value matching the supplied -- | key. retrieve :: String -> m (Either StorageError a) class Storable a where -- | Produces a key from the given `a`. This is most appropriately an `id` or something like it. key :: a -> String -- | When given a proxy of `a` returns a prefix that all items with that type will be stored with. prefix :: Proxy a -> String -- | Takes an `a` and (hopefully) reliably turns it into a string; this should be symmetric with -- | `deserialize`. This can be conveniently implemented using `Simple.JSON`. serialize :: a -> String -- | Takes a string and either successfully decodes it into a `Right a` or returns a -- | `DecodingError` that contains a `NonEmptyList ForeignError`. This matches the error value -- | that `Simple.JSON` returns. deserialize :: String -> Either StorageError a data StorageError = NoValueError String | NoStorageError | DecodeError (NonEmptyList ForeignError) derive instance genRepStorageError :: Rep.Generic StorageError _ instance showStorageError :: Show StorageError where show = genericShow derive instance eqStorageError :: Eq StorageError -- | Given a storable `a` will produce the key that it would have in storage. getKey :: ∀ a. Storable a => a -> String getKey a = prefix (Proxy :: Proxy a) <> ":" <> key a -- | When given a `Proxy a` and a key will produce the corresponding composite key. makeKey :: ∀ a. Storable a => Proxy a -> String -> String makeKey p k = prefix p <> ":" <> k instance monadStorageLocalStorage :: Storable a => MonadStorage LocalStorage a where store a = liftEffect $ localStorage >>= maybeWriteToEffectStorage a retrieve k = liftEffect $ localStorage >>= maybeReadFromEffectStorage k else instance monadStorageSessionStorage :: Storable a => MonadStorage SessionStorage a where store a = liftEffect $ sessionStorage >>= maybeWriteToEffectStorage a retrieve k = liftEffect $ sessionStorage >>= maybeReadFromEffectStorage k else instance monadStorageMonadState :: ( Storable a , MonadState (Map String String) m ) => MonadStorage m a where store a = Right <$> (modify_ $ insert (getKey a) (serialize a)) retrieve k = do state <- get maybe (pure $ Left $ NoValueError k) (pure <<< deserialize) (lookup (makeKey (Proxy :: Proxy a) k) state) maybeWriteToEffectStorage :: ∀ a m . Storable a => MonadEffect m => a -> Maybe Storage -> m (Either StorageError Unit) maybeWriteToEffectStorage a = maybe (pure $ Left NoStorageError) (writeToEffectStorage a) maybeReadFromEffectStorage :: ∀ a m . Storable a => MonadEffect m => String -> Maybe Storage -> m (Either StorageError a) maybeReadFromEffectStorage k = maybe (pure $ Left NoStorageError) (readFromEffectStorage k) writeToEffectStorage :: ∀ a m . Storable a => MonadEffect m => a -> Storage -> m (Either StorageError Unit) writeToEffectStorage a storage = setItem storage (getKey a) (serialize a) readFromEffectStorage :: ∀ a m . Storable a => MonadEffect m => String -> Storage -> m (Either StorageError a) readFromEffectStorage k storage = getItem storage (makeKey (Proxy :: Proxy a) k) >>= either (pure <<< Left) (pure <<< deserialize)
Partly correct. The
liftEffect have nothing to do with FFI specifically. You could with a bit of a stretch say the remaining two do, but fundamentally they’re just functions and constructors in the language itself, not some layer put on top.
MonadEffect m makes it so that this machinery works in any monad that allows for side-effects, which is something I can’t even talk about (certainly not reasonably) in OCaml. As for the
Effect monad itself, yes, it’s an integral part of PureScript and is everywhere. Learning about
Effect is core to the language and isn’t something you would do only because you want to bind to a few libraries. The same can not be said for BS decorators.
With regards to “You could remove that part to get equivalent functionality”, I could probably reduce the PureScript version by a good 50% and get more out of it than the OCaml version, but for good measure I pasted most of the PureScript version up top.
My point here is mainly that there is a nasty language on top of the FFI that is badly thought out, things are badly named and it requires studying that isn’t relevant anywhere else, which isn’t the case if you make your FFI just use the normal facilities of the language. It’s pointless to say “Well, OCaml does it too”, because having to use it doesn’t get any better from knowing that OCamlers came up with it.
A lot of those are exceptionally not needed as a simple function binding and then a helper function to wrap it with a better interface is perfectly sufficient (and what I do for binding C native libraries). I’m not sure why BS added most of those but they aren’t necessary by any stretch.
Both of those are fairly horrifying. ^.^;
First of all, the
@@bs.send stuff is unnecessary (as is technically the
@@bs.return null_to_opt as you can just conver it yourself in a helper.
In addition the
local_storage type is wrong, it should be nullable as not every browser supports it (and the
bs.val is also unnecessary).
Make module is also way overdesigned, you don’t need it at all, just use strings to bring in and out, OCaml already has fantastic facilities for parsing/formatting strings from/to data, that module makes it look like it is trying to use it as a structure, which
localStorage is not and should not be treated as such.
You don’t need to use any of BS’s extensions, and honestly I’m not sure why those keep getting focused on as they are entirely optional (and unreadably superfluous in my opinion, as standard helper functions to access the externals is not only more idiomatically OCaml but far easier to control and read).
BS is not the only option either, JSOO always works quite well, though generates not as readable code, it has significantly easier compat with the rest of the OCaml ecosystem as it uses the standard tools.
There isn’t, I’m not sure who made that OCaml one but it’s pretty bad…
Bucklescript adds the attributes however, the things like
[@@bs.send] and so forth, most of which are not just bad but not easily controllable, all in the name to save the bounce function that traditionally wraps the
external. In general in OCaml an external is just something like
external blah : the -> types -> here = "bound_name", and that’s it, no weird attribute stuff or anything of the sort, and those still work just fine in bucklescript, the attributes are optional.
Not seen that one, though it doesn’t look like what BS is doing, rather it looks like it’s just adding helpers for using the ctypes FFI library in OCaml while also allowing to add some inline C code instead of needing to link a secondary file directly, quite different from BS, though it uses the same extension points (the compiler is pluggable).
Yeah I was wondering about that, but I didn’t know if purescript was implicitly doing that or not, the bucklescript version could be just as simple as (and this is how I would use it):
type t = < length : int [@bs.get]; clear : unit -> unit [@bs.meth]; key : int -> string [@bs.meth]; getItem : string -> string [@bs.meth]; removeItem : string -> unit [@bs.meth]; setItem : string -> string -> unit [@bs.meth]; > Js.t
And that type exposes it all, just grabbing the
localStorage : LocalStorage.t Js.Undefined.t [@bs.get]; from the
For simpler binding so you don’t need those bounce functions (public functions that clean up the interface and usage and args before calling the external itself). I’m not sure why the BS/ReasonML ecosystem is so against them…
Or don’t use them at all. Besides you generally have to know about the system that you are binding to anyway. In vanilla OCaml if you want to link in some C library then you have to know how C exposes the interface and types. You require domain knowledge to do the binding regardless.
However, those bindings only need to be done once then anyone else can use it ‘without’ the domain knowledge.
external is not a BS construct, it is fully vanilla OCaml to just tell the linker to find ‘something’ of that name and type to link in there.
Wow that is significantly larger… Just to do the same thing as the fairly useless OCaml module??
Then don’t, use binding libraries someone else has made. Plenty out there.
What could the purescript version do that my above type cannot do with less code? It’s literally just type definitions… o.O
What about this is superfluous?! This is the basic OCaml external declaration:
external add: int -> int -> int = "add"
And that is all you need to use a function with the name
If that’s actually the case I withdraw my complaints. They’ve done an extremely bad job in emphasizing that you don’t have to use the decorators, to be honest. I was active in the community for some time and not once did it come up or was noted in documentation.
Not once did anyone point out that a simpler version without these decorators was possible (thus also not preferrable) and I wish I’d known this long ago.
The functor is made to be instantiated with a type for each thing you want to store. Standardizing the interface for that and minimizing the amount of plumbing you have to do per call is definitely worth it. I don’t get what you mean by treating
localStorage as a structure.
Yes, the OCaml version lacks prefixing for different types. It’s unfortunate, but fixable by adding a prefix separately or embedding them in the
M.serialize_key function for each type to be stored.
This isn’t doing the same thing as the functor, though… The functor is a way for a type to specify how to store itself. It’s odd that you didn’t get this from the code, to be honest.
If you’re talking about the bigger snippet. No, it does significantly more in many dimensions. Also, you didn’t even get the purpose of that pretty simple functor and what it does so I’m not entirely sure I can even take your comments seriously.
The shorter snippet: Defines the FFI functions to use as well as curried versions for the PureScript interface to use. Yes, it’s longer because of that and could be shorter if the FFI code was written a certain way, but this way is better in the end and is a one-time cost. Optimizing for code length isn’t valuable for FFI plumbing, IMO.
Let’s see; for the longer snippet:
It creates a common interface for what a storable type means; which functions have to be implemented for storing it safely. It creates an interface for what it means to have a monad you can store and retrieve things from a string->string storage mechanism. It defines instances for those for LocalStorage + SessionStorage, plus an in-memory version for a StateMonad using a map. And no, your code obviously does none of that. It doesn’t even do the minor part of ensuring a storable thing that the functor does.
Did you rush so fast to respond that you didn’t even bother checking what the intent of the code was? I could understand you not understanding the PureScript code but to not grasp the OCaml version is a bit odd for someone who reportedly uses it a lot. Also, being that you don’t understand what the PureScript version does, how are you in any position to comment on it?
I feel similarly, the wide range of tools available with Bucklescript’s FFI has always been quite daunting to me.
I wish we had, to be honest, but I wonder if I would’ve had to sneak it in. The only way I managed to convince my PM was by dropping Reason into the conversation a few times and saying Facebook “made it”, then I spent a week or so uploading both Reason versions and OCaml versions (source written in OCaml, converted to Reason via tools) and soon after I
git rmd the Reason code and stopped uploading it.
I did the same with the generated JS for a while but no one actually wanted to read it so that went about the same.
Well specializing modules like that is useful if they are going to be stuffed in to things. For something like that where the key is related to the type I’d honestly just use simple functions, it would be all of 4-6 lines of code depending on how pretty you’d want it.
Can also compile a global key<->type registry to prevent mismatches as well, that would be a fun little thing to do with first class modules and would make it worth making a module version.
I did, but I didn’t care about stuffing in non-strings as localStorage ‘is’ a storage for strings. If I wanted to encode something into a string or back again then I’d just use
ppx_yojson or something instead as it would just be all around a lot easier and a lot less code.
I did, I even remarked about how it was overdesigned just to be able to parse/format the strings into the system from types by passing in a module with defined functions to do the conversion (way WAY overdesigned, simple functions outright would have worked so much better).
I posted the more simple to say what I did in one of my projects, I didn’t need all the string conversion as I handle that in user code. There is no need to pollute the system by making excess modules just to do string<->type conversions when I already have systems to do that for me without remaking it all.
That’s a monad system *“On Top Of” the localStorage calls, that is excess wrappers around the integration that does the actual work of ‘calling’ into the
localStorage object and thus is unrelated to what I asked. What does the purescript FFI do that OCaml’s does not? Whatever you build on top of that you can do in both systems anyway, it is the FFI that differs (ignoring basic language differences like typeclasses vs FCM and so forth that are unrelated to the FFI)?
My code absolutely should not be doing anything like that, that is not what localStorage does nor what it should be encoding.
localStorage is as trustable as user input (since it is freely editable by the user, browser extensions, etc… etc…)
What I inferred from the code is that it is a Functor module that generates a module by taking in a module that fulfills an interface just to call 2 functions on it with highly nonstandard names just to wrap that up to make two function that call the serializers just to store or load a string, when it would be far easier just to have the
localStorage functions themselves and pass in the data serialized by the caller of the function, which likely wouldn’t even be any longer to type either as it’s saving the whole module indirection stuff as well and would be easily simplified if so wished, especially as the user already has to handle an option of a success/failure to load from the
localStorage to begin with, of which they are losing the ability to check if it is because it is empty or whether it is because of serialization failure, which could be very good information to add to logs.
The purescript version is obvious as well, I just didn’t know if it was doing something implicit that was equivalent to the OCaml code as they seemed entirely non-comparable as they seemed to be doing entirely different things, and as I haven’t used purescript outside of simple test (soooo sloooow to compile…) I didn’t know if it had some ‘magic’ that it was doing to do the same functionality as I wouldn’t have expected two pieces of code that were being compared to do two entirely and significantly different things.
Yeah BS has gone way way overboard with attributes all in the name of just saving simple bounce functions, I don’t get it at all… >.>
Facebook also made the JSOO and Dune integration with ReasonML as well (even before BS, and they’ve kept it up very well). BS is not tied to ReasonML by any stretch, especially as ReasonML is just a simple stringly transcompiler that knows nothing about types, only syntax, thus it would work with Any OCaml backend and Any OCaml pipeline. The Dune/JSOO integration they did is just a simple set of helpers to add it to a project with a single line, otherwise you’d need to wire it up yourself with like 4 lines in the builldscripts.
There are no ties from ReasonML to BS at all, BS has ties to ReasonML since it includes ReasonML with it, but even that is entirely optional to use (I think that was only done to try to make BS more popular).