Recently I successfully completed a crazy idea: To write some ocaml functions to use inside Google Apps Script for a small stupid spreadsheet that I had.
The way it works now is by having a main index.js file that calls the Ocaml functions that are available under a global Lib namespace. Everything is bundled using parcel and the Idea was to use as few JS code as possible. Because it was easier than I expected I decided to go one step further and write some bindings for the GAS functions I was using and reduce the glue JS code even more.
This are the bindings that I wrote so far. They work, but are not usable inside Ocaml yet.
external getActiveSpreadsheet : unit -> spreadsheet = "getActiveSpreadsheet" [@@bs.val][@@bs.scope "SpreadsheetApp"]
external getSheets : spreadsheet -> sheet array = "getSheets" [@@bs.send]
external getSheetByName : spreadsheet -> string -> sheet = "getSheetByName" [@@bs.send]
external getDataRange : sheet -> range = "getDataRange" [@@bs.send]
external getValues : range -> 'a array array = "getValues" [@@bs.send]
My doubt are on the edges. When it is just obscure GAS stuff I have no doubt, abstract types and functions to interact with them. Is when a GAS function returns data where I have doubts. Usually they are just arrays of arrays of Numbers or Strings. In the example above, the last definition says that you will get an array of arrays of 'a, but that is not true because it will be an array of “stuff” (strings, numbers, floats).
How should I type it in a way that it’s flexible but not cumbersome? For example, I don’t think using a functor will help because you will need to create a functor for every possible return type, in my case if you have 3 sheets with 3 different shapes, you will need 3 functors.
An alternative that I have used was to provide some helper functions to convert from JS to Ocaml types and then unwrap the Ocaml types. If you take a look at the project on github that’s what I’m doing with Number_or_string.
This is nothing serious and I will just add the bindings that I may need for now, but I want to hear what the community (and potential users) thinks.
Thanks for the answer @ts
This looks like an improvement over what I already have. My question is, why do I need the rawCellToXX functions and how are they type safe if they are %identity bindings? I guess the type safe is coming from the classify function and those two functions are just a way to avoid runtime tag checking?
Those may be useful when you are sure about the type of value you will get (not the case) and the array is homogeneous (neither the case).
I’m targeting safety with this Api specially because it is just one single contact point that I want to be as “secured” as possible.
Interestingly I have this code that I modified from BuckleScript blog:
module Number_or_string = struct
type t = Any : 'a -> t
type case =
| Int of int
| String of string
let classify (Any v : t) : case =
if Js.typeof v = "number" then Int (Obj.magic v : int)
else String (Obj.magic v : string)
And it produces exactly the same JS output that the solution @tsnobip proposed, but the implementation is way more complex and uses more (to me at least) obscure BuckleScript features. Is it any better than the simpler solution of using rawCellToFloat and rawCellToString?
This solution requires less Obj.magic, “%identity” is a type hole by itself.
uses more (to me at least) obscure BuckleScript features
If you put an explicit type signature to the module Number_or_string, I think that’s fine whether how complex the internal is, that’s the beauty of module system, you only need care about the interface not the implementation
@danielo515Obj.magic and identity do the same thing, they force the type-checker to give a value a given type, so it’s a trick and you have to be extremely cautious with it. But identity is slightly safer since you have to define beforehand from which and to which type you want this conversion to be done, I would advise to prefer identity over Obj.magic thanks to this safeguard.
unboxed types are mostly used when you want to construct a heterogeneous type as input of a JS function, but you don’t need this here since the heterogeneous types you’re binding to are output values, a regular abstract type is enough.
Thank you very much! This clarifies things a lot. By the way, where can I find more information about unboxed? I just saw a blog post and it was not enough to give me a clear idea of what it is useful for
When creating bindings to a library, %identity is definitely better as zero-cost function calls are ideal. But when writing some code that is doing something potentially dangerous, I prefer Obj.magic to make it clear to anyone reading the code later that there is a big risk here.
Semantically there’s no difference, it’s a code style and personal preference. I find additional external %identity definitions annoying, both to create and when I forget I’m using one. In cases like this, if you forget to hide the %identity functions with an interface file, and one of them is used elsewhere, it might break the type system.
Personally I would combine the approaches discussed here; go with type rawCell rather than unboxed, but leverage Obj.magic in the classify function instead of using externals.
They’re equally dangerous; with %identity you’re just defining what you want the types to be. It’s no safer than Obj.magic, and in fact it can give a false impression for new developers who don’t yet understand the insane power and risk at play here.
I will admit that once I settle on the bindings to a library I codify API conversions with %identity, but for internal conversions between types (and particularly as a shortcut in tests) I reach to Obj.magic.