ReScript syntax for generative functors

I’m trying to convert a generative functor to the new syntax. Here’s the Reason code:

module type Primitive = {
  type t;
  let compare: (t, t) => int;
}

module Fresh = (P: Primitive, ()) :
({
  type t;
  type internal;
  let inject: internal => t;
  let project: t => internal;
  let compare: (t, t) => int;
  let equal: (t, t) => bool;
} with type internal = P.t) => {
  type t = P.t;
  type internal = t;
  external inject: internal => t = "%identity";
  external project: t => internal = "%identity";
  let compare = P.compare;
  let equal = (a, b) => P.compare(a, b) == 0;
}

But when I try to convert it to the new syntax, ReScript parser seems to have issue with the unit argument in this line:

module Fresh = (P: Primitive, ()) :

Can it be done in ReScript at all? Will it be supported in the future? Are there workarounds to make the numerous Fresh(String).t types incompatible (other than using Reason/OCaml syntax for the functors)?

Potentially, maybe it would be even nicer if ReScript supported units of measure, like F#. Then again, units of measure don’t suppose parsing (I think), and there are a lot of cases when a valid primitive value is not a valid entity value.

Can you file an issue on the syntax repo? I’ll have a look. Generative functors are quite niche, what’s your use case?

1 Like

Yes, there is a workaround: to annotate the Fresh(...) output modules at the point of binding them. This forces the compiler to throw away its knowledge that the t types are the same. E.g.:

module type Primitive = Set.OrderedType;

module type FRESH = {
  include Primitive;
  type internal;

  let inject: internal => t;
  let project: t => internal;
  let equal: (t, t) => bool;
};

module Fresh(P: Primitive) = {
  type t = P.t;
  type internal = t;

  let inject = internal => internal;
  let project = t => t;
  let compare = P.compare;
  let equal = (a, b) => compare(a, b) == 0;
}

module M1: FRESH with type internal = string = Fresh(String);
module M2: FRESH with type internal = string = Fresh(String);

let m1 = M1.inject("");
let m2 = M2.inject("");
let _ = M1.compare(m1, m2);

This gives:

We've found a bug for you!
OCaml preview 24:23-24

This has type:
  M2.t
But somewhere wanted:
  M1.t
1 Like

There, one issue :slight_smile:

The use case is to have zero-cost distinct types (so you cannot use name instead of email, account number instead of transaction sum, etc. As I said, I’m probably OK if ReScript has first-class support for those cases one day, but today, as far as I know, OCaml cannot give you this functionality without applicative functors at all.

So… Fresh(String) has the same module type as Fresh(String), but types of FRESH with type internal = string are distinct? :thinking:

Thanks a lot for the workaround, I’m just pondering how the type system works.

If we don’t annotate the module types, the compiler infers a ‘full’ type of the module, including the relation that t = string on both the modules. But if we annotate the module type, then it’s restricted to only the information that’s available from the module type FRESH with type internal = string. In this module type, there is no relation type t = string, only type t. And the compiler doesn’t equate type t from different modules as the same type.

2 Likes

Makes sense. Thanks for the explanation!

Np. One more small but neat trick: you can annotate the modules with type:

module M1: FRESH with type internal := string = Fresh(String)

The := does a destructive substitution, meaning the type internal is erased from the final module type. So it becomes effectively:

module M1: {
  include Primitive
  let inject: string => t

  let project: t => string
  let equal: (t, t) => bool
}

Neat indeed. Thank you :pray:

1 Like

@hoichi Hi, are you available? Maybe you can send a DM? I’m interested to buy a similar script

I dont know if you heard about redux-saga. The reason I am looking at rescript is because typescript does not work well with redux-saga. It will color whole files in red, because it cant deal with sagas the way it should. Sagas are generators that get advanced ( gen.next() calls) by the redux-saga redux middleware. It sounds weird but is actually a super awesome library and solves my problem. ( I work on a browserwallet atm)
The only downside is that I am stuck with javascript without any typesystem because of this issue.
I think if rescript could handle this usecase very well, this would be a huge selling point. browser wallets and these kinds of more complex browser plugins in general have huge potential for growth.
I would also offer to create some propaganda about this, if we manage to find a beautiful solution to this problem.
Thanks, and hello everyone (this is my first post here lol :grinning: )
Kind Regards,
Spirobel