How to tell the compiler to inline a function call

This generates very clean 0 cost JavaScript:

module A = {
  type t
  external str: string => t = "%identity"
  external int: int => t = "%identity"
  @val external doThings: t => unit = "doThings"
}
A.doThings(A.str("no cost"))
A.doThings(A.int(8))

generated js:

doThings("no cost");
doThings(8);

But if you add types:

module B: {
  type t
  let str: string =>t
  let int: int => t
  let doThings: t => unit
} = {
  type t
  external str: string => t = "%identity"
  external int: int => t = "%identity"
  @val external doThings: t => unit = "doThings"
}

You get a much more complicates output, with function and curry calls:

function B_str(prim) {
  return prim;
}
function B_int(prim) {
  return prim;
}
function B_doThings(prim) {
  doThings(prim);
}
var B = {
  str: B_str,
  $$int: B_int,
  doThings: B_doThings
};
Curry._1(B_doThings, Curry._1(B_str, "no cost"));
Curry._1(B_doThings, Curry._1(B_int, 8));

How can you tell the compiler in the module B that the type/function can/should be inlined, so that you get the same output as module A?

Playground Link

to clarify - if you repeat the same code in the type - it works, but this looks very weird - to have to put the implementation in the type:

module C: {
  type t
  external str: string => t = "%identity"
  external int: int => t = "%identity"
  @val external doThings: t => unit = "doThings"
} = {
  type t
  external str: string => t = "%identity"
  external int: int => t = "%identity"
  @val external doThings: t => unit = "doThings"
}

There’s a trick you can use to save some boilerplate in this case (a module with only types and externals), use a recursive module and make it refer to itself:


module rec C: {
  type t

  external str: string => t = "%identity"
  external int: int => t = "%identity"
  @val external doThings: t => unit = "doThings"
} = C
1 Like

The behavior here is by design, AFAIK. If you expose a value as a let binding in the interface, then outside code has no way of knowing how it’s implemented, and so it can’t inline it as an external. The solution is what you’ve already discovered, to expose the bindings as external in the interface.

This can lead to code duplication, but if your module is entirely zero-cost types and externals, then is an interface necessary?

2 Likes

the issue is that in the real-world example I am not exporting everything from the module

if you add a .resi file - it is the same issue

I guess this is my question - what is the syntax to tell the compiler whether something should be optimized / inlined

It is not a very pretty solution - it is putting implementation details like @val external doThings: t => unit = "doThings" in module types and .resi files

a module type allows you to hide internal types and values (the same issue occurs when you use .resi files as well)

for example

module MyModule: {
  type t
  let make: unit => t
  let doThings: t => unit
} = {
  type t = string
  let random = x => ...
  let make = () => random(5)
  let doThings = x => ...
}

Yeah, there’s no perfect solution here. If you need to have private items in the module, you need to use an interface file. If you want to expose zero-cost bindings from the interface file, you need to copy the externals there.

Another way to approach this that keeps the generated JS calls pretty clean, but also lets you hide certain things and not have to type the externals twice would be to stick your FFI externals into their own module and include them. (Here’s a link to the playground.)

// same recursive module trick that yawaramin showed above
module rec SillyFfi: {
  type t

  @module("silly") external make: string => t = "make"
  @module("silly") external foo: (t, int) => t = "foo"
  @module("silly") external bar: t => t = "bar"
} = SillyFfi

module Silly: {
  // include the signatures in this module
  include module type of SillyFfi

  // If you need to keep the types equal to the original SillyFfi module
  // for some reason (i.e., type strengthening), you can use this instead.
  //
  //   include module type of {
  //     include SillyFfi
  //   }

  // Additional stuff if needed.
  let baz: (t, string) => t
} = {
  // and include the implementation too
  include SillyFfi

  // private
  @module("silly") external internal1: t => t = "internal1"
  // private
  @module("silly") external internal2: t => t = "internal2"
  // uses private functions
  let baz = (t, _s) => internal1(t)->internal2
}

let _x = Silly.make("yo!")->Silly.foo(123)->Silly.bar->Silly.baz("whatever")

The part of the generated JS where the function calls happen is this:

var t = Silly.bar(Silly.foo(Silly.make("yo!"), 123));
var _x = Silly.internal2(Silly.internal1(t));

I didn’t see anything in the FAQ regarding whether it’s okay to revive an old thread or not, so hopefully it is fine!

Edit: Here’s a link to a similar discussion for reference: Curious about JS output for different ways to define a module - #2 by yawaramin