Issues with currying externally define function

Hello!

I’m trying to integrate with the library zustand as a test of Rescript, but I seem to be having some trouble currying.

type store = {
  "count": int,
  "onClick": () => ()
}
type storeInitializer = (store => (), () => store) => store 
type storeSelector = store => store
@bs.module("zustand") external create: (storeInitializer) => (storeSelector => store) = "default"

let storeInitFn: storeInitializer = (set, get) => {
  "count": 0,
  "onClick": () => {
    set({ "onClick": get()["onClick"], "count": 1 + get()["count"] })
  }
}
let useStore: storeSelector => store = create(storeInitFn)

Here’s the playground link. Currently, it’s calling zustand's default export as though it were an arity 2 function in JavaScript:

function useStore(param) {
  return Zustand(storeInitFn, param);
}

whereas I want it to output useStore in the form

let useStore = Zustand(storeInitFn);

Is this what you’re trying to achieve?

Parenthesis in a type definition like your external don’t really matter to the compiler because functions are curried by default.
Meaning both are equal:

external a: x => y => z = "fn"
external b: (x => y) => z = "fn"

In your case, the easiest solution is to use your useStore type inside of your external create definition.

Edit: You could also use the explictly incurried solution: https://rescript-lang.org/docs/manual/latest/bind-to-js-function#extra-solution
Edit2: Just tried the extra-solution in the playground: this is only usefull if your function is an argument, not the return value. But if you explicitly uncurry your ‘useStore’ type, you won’t get a Curry call during runtime: example using uncurried useStore

2 Likes

Oh interesting. Does that mean type signatures don’t follow the substitution principle (or perhaps just for types on external functions)? That would be very surprising to me. I get that => in function signatures is right associative.

Thank you for your help!

To my knowledge this behavior (differentiation of an extra type vs inline types) is only in external definitions.
Otherwise (non-externals) a function of

let f: (string => string) => (string => string)

would behave the same way if it were defined like

type returnFn = string => string
let f: (string => string) => returnFn

Somewhat related:

What would be the best way to represent an (external) JS function with 1 mandatory param and one optional boolean param, e.g. (a, b = false) => { ... } ? I’ve tried:

  • Two ordered params (a, b: bool) => ...: this works but that means that you have to pass the 2nd param every time
  • One ordered param and one optional keyword param (~b: bool=?, a): this doesn’t work as it seems that in actual application, when the default value is not overridden (i.e. the fn is passed one param) it passes the ordered param second (which I suppose makes sense?) and passes undefined as the first param.
  • One ordered param, one optional keyword param, and unit: (a, ~b: bool=?, unit): this doesn’t really have any advantage over the first idea as it would be called with a and ()…and it doesn’t work–a call like fn(foo, ()) attempts to pass undefined to the return value of the externally defined fn

The third method should work. Can you show some sample code and output?

Your right: example
I guess if the js api can’t handle undefined as arg, I’d probably use two external definitions.
If you don’t mind an extra function call you could then wrap the two externals in one function call with your desired api.

Unfortunately this is not part of the top-level API of zustand (the package i’m interacting with)–it’s the signature of a function that it passes in as a param (it’s the set function in my earlier example) so I’m not sure if you can use external for it.

Here’s a playground link.

The issue is that in the Curry._3 call: we have Curry._3(set, newState, undefined, undefined). Since set has an arity of 2, Curry._3 attempts to call the return value of set(newState, undefined) with undefined, but set returns undefined, so it errors.

It seems like it’s not an issue for Zustand if you call it’s set function with set({someObj}, undefined, undefined): Codesandbox
Therefore I think the issue is with the curry call.

You could take inspiration from the react bindings and always type to the most secure version of a function: Meaning just type set as (.store, bool) => unit

I adapted your example to prevent any currying here.


I looked at Zustand's api: The issue with Zustand is, that it’s highly polymorphic. It’s hard to create general purpose bindings for such js apis in general. (if your goal is to still have accurate types)

You would either have a lot of unsafe types or specific types for your concrete use-case. - like your types in your example

Do you have a specific use-case in your code-base for this library or are you trying to come up with general purpose bindings?
What is the reason, you’re prefering to use Zustand?

Have you seen the useReducer Hook?
(WIP-PR) new documentation of reason-react & useReducer-hook

edit: I don’t really like the resulting code of my example, but it showcases explicit uncurrying and should probably work?

I’m just exploring Rescript. I was interested in the appeal of a lang that sat somewhere between Elm & Typescript on the continuum of permissiveness vs. safety. Zustand was a more of a test of “how difficult would it be to use a very dynamic external library written in JS with Rescript?” For a green-field project though, you’re right, I would probably use useReducer or reductive.

Thank you for your help.