Bindings for GAS functions that returns arrays

Hello,
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.

type spreadsheet
type sheet
type range
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.

If anyone is interested in taking a look on the project, it is here: https://github.com/danielo515/ElectronicProjectsSpreadsheet

Regards

what you mean is that inside a range, all the cells are not of the same type, right? Then indeed 'a is not a solution since your arrays would be heterogeneous.

I would indeed go with the unwrapping you suggested.

Something like this:

type rawCell
external getValues : range -> rawCell array array = "getValues"  [@@bs.send]
external rawCellToFloat: rawCell -> float = "%identity"
external rawCellToString: rawCell -> string = "%identity"

type cell = 
        | Number of float 
        | String of string
let classify (rawCell : rawCell) : cell = 
        if Js.typeof v = "number" then Number (rawCellToFloat(rawCell))
        else String (rawCellToString(rawCell))

Hello! For these small number of permutations, you can just do this:

external getValuesInt : range -> int array array = "getValues"  [@@bs.send]
external getValuesString : range -> int array array = "getValues"  [@@bs.send]

No runtime, still clear output. More runtime = more debugging area, which is a much more tedious thing to watch out for than a tiny bit of duplication.

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?

Hello cheng,
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 
    [@@unboxed]     
    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)
end

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

Not sure if this is an argument in favour of Obj.magic or the opposite. Is Obj.magic better? if so, why?

@danielo515 Obj.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.

1 Like

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

@danielo515 you probably read https://reasonml.org/blog/union-types-in-bucklescript, honestly I don’t think there’s much more to it than binding to JS heterogeneous input types.

Yes that’s it. I think it is just a matter of let it click as I continue to use Bucklescript

I think the choice depends on the context.

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.

In an example like the one presented here, where I’m doing a type check on the JS side, whee is the risk? I’m asking honestly

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.

I agree with @tsnobip that %identity is in general preferable over Obj.magic.

IMHO best practice would be defining

external xToY: x => y = "%identity";

if the conversion from type x to type y is safe and

external unsafeXToY: x => y = "%identity";

otherwise.

Obj.magic is simply

external magic: 'a => 'b = "%identity";

and thus much more dangerous than if the types are explicitly specified.

1 Like

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.

IMHO that’s not right.

Let’s say I do a refactoring and I inadvertently change type of the thing I am “casting”. The explicitly typed conversion functions will catch that, Obj.magic won’t.

(Been there, done that. :wink:)

3 Likes

Woah this thread is still going

Yes, prefer explicit %identity cast over Obj.magic. The state space of former is much smaller than the latter. There’s not much reason to use the latter.

1 Like

thanks everyone for the clarifications about %identity, learnt a lot