"recursion" in a function argument via callback

I have a function, that returns another function, say

let registerSW = (opts) => () => ()

the return type of the registerWS is a function to update service worker, let’s call it updateSW. In the opts I’m passing a React Element, that renders a button, which calls the updateSW on click.

let updateSW = registerSW({ foo: <button onClick={_ => updateSW() }/> })

So the function is kind of recursive but it refers to itself in a function argument, not in it’s body, and it does that via another callback - the onClick handler. Something perfectly valid in JS is invalid in Rescript. The rec keyword does not help for there is no function declaration, but function invocation. The function is external, co I cannot declare it as rec because again, bindings are not function declaration. And because I refer to the return type in function argument, it is not a recursion in a classic sense. I’m currently using %raw to escape the error, but I am curious if there is a way to write this in a valid Rescript?

Can you give the full example of what you are trying to do? Is just the registerSW function external or updateSW as well? You are not using any external keyword here.

If the signature of “registerSW” is like you wrote then the update function can be written like this:

type opts = {foo: React.element}

@val
external registerSW: opts => unit => unit = "registerSW"

let rec updateSW = () =>
  registerSW({foo: <button onClick={_ => updateSW()} />})->ignore

The updateSW is a return of the registerSW, so it is intrinsically external as the registerSW is external. I’m loading it from the Vite PWA plugin, the binding is like

@module("virtual:pwa-register")
external registerSW: registerSWOptions => unit => unit = "registerSW"

and let’s say the registerSWOptions has the type

type registerSWOptions = {foo: React.element}

for simplicity.

I name the return type () => () a updateSW but it is only my assignment.

let updateSW = registerSW({ foo: <button onClick={_ => updateSW() }/> })

Your example would not work, as the updateSW calls registerSW in it’s body, but it should not do that - the updateSW is simply and external function returned by the registerSW.

Now I get it, your problem is not really recursion itself but immutability. You can do it with a ref.

type opts = {foo: React.element}

@val
external registerSW: opts => unit => unit = "registerSW"

let updateSWRef = ref(ignore)

updateSWRef :=
  registerSW({foo: <button onClick={_ => updateSWRef.contents()} />})
1 Like

That helps, thanks! I did not suspected an immutability to be a problem, since I never reassigned the let updateSW. I suspected the type system to be a cause, as it encountered a cycle while resolving the expression and it generally don’t handle that without rec keyword which only works for function declaration - that is why I used the term “recursion” in the title.

Using ref(ignore) is another interesting trick, I would assume that the type of the ref would be locked to ignore but it later allows storing a function there, so ref(ignore) is like a void assignment that just prepares the mutable variable. I tried assign the updateSW to the ref without the ref(ignore) preparation but it did not work, apparently for the same reason mentioned above - that the type system was unable tho resolve a type of the function which points to its own result in it’s argument.

Long story short, this surprised me and it is worth documenting! I will make a PR to documentation when I will find some spare time! Meantime, please help me understand what is going on when you assign ref(ignore), this part is still little bit blurry to me.

So ignore has the following signature: 'a => unit, it will only work for functions that have one arbitrary parameter and return unit. In your case, updateSW has the signature unit => unit which can be unified by the type checker without errors.

If you had a more complex function like

@val external registerSW: opts => (string, int) => unit = "registerSW"

You would probably initialize it wrapped in an option:

let updateSWRef = ref(None)

updateSWRef :=
  Some(
    registerSW({
      foo: <button
        onClick={_ => {
          updateSWRef.contents->Option.forEach(updateSWRef =>
            updateSWRef("abc", 123)
          )
        }}
      />,
    }),
  )

And the ignore function is equivalent to this:

let ignore = _param1 => ()

So you could also create your own ignore functions if the signature of the built-in one does not match.

let ignore2 = (_param1, _param2) => ()

Caveat: The compiler won’t optimize them away like the built-in one.

Does that clarify it?