Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NotForNow] GenType: explore the idea of using namespaces to represent modules. #6117

Closed
cristianoc opened this issue Apr 9, 2023 · 14 comments
Labels
experiment stale Old issues that went stale
Milestone

Comments

@cristianoc
Copy link
Collaborator

Explore a cleaner way to represent ReScript modules in TypeScript using namespaces

Background

In ReScript, modules are used to organize code and encapsulate related types and values. ReScript modules can contain values (such as functions) and type declarations, allowing users to access them using the syntax M.t for a type t defined inside module M. Modules can also be nested, and ReScript supports functors (functions between modules).

Currently, genType maps ReScript modules to TypeScript constructs, but there might be a cleaner way to represent them using TypeScript namespaces.

Proposed Solution

The proposed solution is to represent ReScript modules and their features in TypeScript using namespaces:

  1. Representing ReScript modules: Use TypeScript namespaces to group related types and values.

    namespace M {
      export type t = {
        // Your type definition goes here
      };
    
      export function someFunction(): t {
        // Your function implementation goes here
      }
    }
  2. Representing nested ReScript modules: Use nested TypeScript namespaces.

namespace M {
  export type t = {
    // Your type definition goes here
  };

  export function foo(): t {
    // Your function implementation goes here
  }

  export namespace N {
    export type t = {
      // Your type definition goes here
    };

    export function bar(x: M.t): t {
      // Your function implementation goes here
    }
  }
}
  1. Accessing a namespace defined in another file: Export the namespace from the source file and then import it in the file where you want to access it.
// Somefile.ts
export namespace SomeModule { /* ... */ }

// AnotherFile.ts
import { SomeModule } from './Somefile';
  1. Mimicking ReScript functors: Use higher-order functions that take objects representing namespaces as input and return objects representing namespaces.
// Define functor-like higher-order function
function myFunctor(input: typeof InputNamespace): typeof OutputNamespace { /* ... */ }

// Use the functor-like higher-order function
const outputModule = myFunctor(InputNamespace);
@cristianoc cristianoc added this to the v11.0 milestone Apr 9, 2023
@cristianoc
Copy link
Collaborator Author

Screenshot 2023-04-09 at 05 31 01
Screenshot 2023-04-09 at 05 31 30
Screenshot 2023-04-09 at 05 31 51

@cometkim
Copy link
Member

It will not work with transpilers like esbuild or Babel. Even if use tsc, it hurts the possibility of tree-shaking of build artifacts.

@cristianoc
Copy link
Collaborator Author

It will not work with transpilers like esbuild or Babel. Even if use tsc, it hurts the possibility of tree-shaking of build artifacts.

That's interesting. Can you expand on that?

@cometkim
Copy link
Member

It will not work with transpilers like esbuild or Babel.

OK, it's outdated. @babel/plugin-transform-typescript and esbuild both support transpiling TS namespaces.

However, it is not "fully" supported, and it is not a recommended option. Babel recommends using modules where possible.
https://babeljs.io/docs/babel-plugin-transform-typescript#impartial-namespace-support

it hurts the possibility of tree-shaking of build artifacts.

I already rely on the GenType to generate TypeScript modules in ES Module format.
https://github.com/reason-seoul/rescript-collection/blob/main/examples/ts-rescript-vector/src/index.ts

When I use the library, the bundler can remove the parts I don't use. Module semantics make easy to do that.

But, the result of namespace:

// generated by tsc
"use strict";
var M;
(function (M) {
    function foo() {
        // Your function implementation goes here
    }
    M.foo = foo;
    let N;
    (function (N) {
        function bar(x) {
            // Your function implementation goes here
        }
        N.bar = bar;
    })(N = M.N || (M.N = {}));
})(M || (M = {}));

Usually, it comes as an IIFE that negates most DCE tools. Most library authors avoid using TS namespaces. The only time it's valid is when used in only type-level and not in runtime code.

@cristianoc
Copy link
Collaborator Author

It will not work with transpilers like esbuild or Babel.

OK, it's outdated. @babel/plugin-transform-typescript and esbuild both support transpiling TS namespaces.

However, it is not "fully" supported, and it is not a recommended option. Babel recommends using modules where possible. https://babeljs.io/docs/babel-plugin-transform-typescript#impartial-namespace-support

it hurts the possibility of tree-shaking of build artifacts.

I already rely on the GenType to generate TypeScript modules in ES Module format. https://github.com/reason-seoul/rescript-collection/blob/main/examples/ts-rescript-vector/src/index.ts

When I use the library, the bundler can remove the parts I don't use. Module semantics make easy to do that.

But, the result of namespace:

// generated by tsc
"use strict";
var M;
(function (M) {
    function foo() {
        // Your function implementation goes here
    }
    M.foo = foo;
    let N;
    (function (N) {
        function bar(x) {
            // Your function implementation goes here
        }
        N.bar = bar;
    })(N = M.N || (M.N = {}));
})(M || (M = {}));

Usually, it comes as an IIFE that negates most DCE tools. Most library authors avoid using TS namespaces. The only time it's valid is when used in only type-level and not in runtime code.

Thanks, this simplifies the design a lot -- no need to explore namespaces further!

I'll ask you some more gentype-related questions too.
What about the opportunity of generating .d.ts files? I assume it would simplify a lot of things. But it's just an assumption.

@cometkim
Copy link
Member

You mean only create .d.ts files for .bs.js instead of TS wrappers? I think that would make sense if we make the JS representation more directly usable.

GenType is actually doing the mapping of the runtime representation as well as adding an alias to the typename. I still have to rely on it.

An example code I have.. source:

@genType
let hasNoAccounts = async (~countAllMembers) => {
  switch await countAllMembers(.) {
  | count => Ok(count == 0)
  | exception Js.Exn.Error(exn) => Error(#IOError({"exn": exn}))
  }
}

JS result:

async function hasNoAccounts(countAllMembers) {
  var count;
  try {
    count = await countAllMembers();
  }
  catch (raw_exn){
    var exn = Caml_js_exceptions.internalToOCamlException(raw_exn);
    if (exn.RE_EXN_ID === Js_exn.$$Error) {
      return {
              TAG: /* Error */1,
              _0: {
                NAME: "IOError",
                VAL: {
                  exn: exn._1
                }
              }
            };
    }
    throw exn;
  }
  return {
          TAG: /* Ok */0,
          _0: count === 0
        };
}

GenType result:

export const hasNoAccounts: (_1:{ readonly countAllMembers: (() => Promise<number>) }) => Promise<
    { tag: "Ok"; value: boolean }
  | { tag: "Error"; value: { readonly exn: Js_Exn_t } }> = function (Arg1: any) {
  const result = Council_Service_AccountBS.hasNoAccounts(Arg1.countAllMembers);
  return result.then(function _element($promise: any) { return $promise.TAG===0
    ? {tag:"Ok", value:$promise._0}
    : {tag:"Error", value:$promise._0}})
};

BTW I'm pretty sure the output can be simplified. It adds too much overhead today.

GenType result in the real-world
export const verifyMemberSession: <T1>(_1:{
  readonly findSession: ((_1:Council_Entity_Session_id) => Promise<(null | undefined | Council_Entity_Session_t)>); 
  readonly findMember: ((_1:T1) => Promise<(null | undefined | Council_Entity_Member_t)>); 
  readonly sessionId: (null | undefined | Council_Entity_Session_id); 
  readonly memberId: (null | undefined | T1)
}) => Promise<
    { tag: "Ok"; value: { readonly member: Council_Entity_Member_t; readonly session: Council_Entity_Session_t } }
  | { tag: "Error"; value: 
    { NAME: "IOError"; VAL: { readonly exn: Js_Exn_t } }
  | { NAME: "InvalidMember"; VAL: { readonly member?: T1 } }
  | { NAME: "InvalidSession"; VAL: { readonly session?: Council_Entity_Session_id } } }> = function <T1>(Arg1: any) {
  const result = Curry._4(Council_Service_SessionBS.verifyMemberSession, function (Arg11: any) {
      const result1 = Arg1.findSession(Arg11);
      return result1.then(function _element($promise: any) { return ($promise == null ? undefined : {_RE:$promise._RE, id:$promise.id, seq:$promise.seq, events:$promise.events.map(function _element(ArrayItem: any) { return ArrayItem.tag==="Created"
        ? Object.assign({TAG: 0}, ArrayItem.value)
        : Object.assign({TAG: 1}, ArrayItem.value)}), state:($promise.state == null ? undefined : $promise.state.tag==="Anonymous"
        ? Object.assign({TAG: 0}, $promise.state.value)
        : Object.assign({TAG: 1}, $promise.state.value))})})
    }, function (Arg12: any) {
      const result2 = Arg1.findMember(Arg12);
      return result2.then(function _element($promise1: any) { return ($promise1 == null ? undefined : {_RE:$promise1._RE, id:$promise1.id, seq:$promise1.seq, events:$promise1.events.map(function _element(ArrayItem1: any) { return ArrayItem1.tag==="Created"
        ? Object.assign({TAG: 0}, ArrayItem1.value)
        : ArrayItem1.tag==="SingupApproved"
        ? Object.assign({TAG: 1}, ArrayItem1.value)
        : ArrayItem1.tag==="SingupRejected"
        ? Object.assign({TAG: 2}, ArrayItem1.value)
        : ArrayItem1.tag==="AdminGranted"
        ? Object.assign({TAG: 3}, ArrayItem1.value)
        : ArrayItem1.tag==="AdminRevoked"
        ? Object.assign({TAG: 4}, ArrayItem1.value)
        : ArrayItem1.tag==="JoinedToOrganization"
        ? Object.assign({TAG: 5}, ArrayItem1.value)
        : ArrayItem1.tag==="LeaveFromOrganization"
        ? Object.assign({TAG: 6}, ArrayItem1.value)
        : ArrayItem1.tag==="Reactivated"
        ? Object.assign({TAG: 7}, ArrayItem1.value)
        : Object.assign({TAG: 8}, ArrayItem1.value)}), state:($promise1.state == null ? undefined : $promise1.state.tag==="Requested"
        ? Object.assign({TAG: 0}, $promise1.state.value)
        : $promise1.state.tag==="Rejected"
        ? Object.assign({TAG: 1}, $promise1.state.value)
        : $promise1.state.tag==="Active"
        ? Object.assign({TAG: 2}, $promise1.state.value)
        : Object.assign({TAG: 3}, $promise1.state.value))})})
    }, (Arg1.sessionId == null ? undefined : Arg1.sessionId), (Arg1.memberId == null ? undefined : Arg1.memberId));
  return result.then(function _element($promise2: any) { return $promise2.TAG===0
    ? {tag:"Ok", value:{member:{_RE:$promise2._0.member._RE, id:$promise2._0.member.id, seq:$promise2._0.member.seq, events:$promise2._0.member.events.map(function _element(ArrayItem2: any) { return ArrayItem2.TAG===0
    ? {tag:"Created", value:ArrayItem2}
    : ArrayItem2.TAG===1
    ? {tag:"SingupApproved", value:ArrayItem2}
    : ArrayItem2.TAG===2
    ? {tag:"SingupRejected", value:ArrayItem2}
    : ArrayItem2.TAG===3
    ? {tag:"AdminGranted", value:ArrayItem2}
    : ArrayItem2.TAG===4
    ? {tag:"AdminRevoked", value:ArrayItem2}
    : ArrayItem2.TAG===5
    ? {tag:"JoinedToOrganization", value:ArrayItem2}
    : ArrayItem2.TAG===6
    ? {tag:"LeaveFromOrganization", value:ArrayItem2}
    : ArrayItem2.TAG===7
    ? {tag:"Reactivated", value:ArrayItem2}
    : {tag:"Deactivated", value:ArrayItem2}}), state:($promise2._0.member.state == null ? $promise2._0.member.state : $promise2._0.member.state.TAG===0
    ? {tag:"Requested", value:$promise2._0.member.state}
    : $promise2._0.member.state.TAG===1
    ? {tag:"Rejected", value:$promise2._0.member.state}
    : $promise2._0.member.state.TAG===2
    ? {tag:"Active", value:$promise2._0.member.state}
    : {tag:"Inactive", value:$promise2._0.member.state})}, session:{_RE:$promise2._0.session._RE, id:$promise2._0.session.id, seq:$promise2._0.session.seq, events:$promise2._0.session.events.map(function _element(ArrayItem3: any) { return ArrayItem3.TAG===0
    ? {tag:"Created", value:ArrayItem3}
    : {tag:"MemberConnected", value:ArrayItem3}}), state:($promise2._0.session.state == null ? $promise2._0.session.state : $promise2._0.session.state.TAG===0
    ? {tag:"Anonymous", value:$promise2._0.session.state}
    : {tag:"Member", value:$promise2._0.session.state})}}}
    : {tag:"Error", value:$promise2._0}})
};

@cristianoc
Copy link
Collaborator Author

GenType is actually doing the mapping of the runtime representation as well as adding an alias to the typename. I still have to rely on it.

Not anymore. In v11, the runtime representation is tunable in the language, and genType does no conversion.

@cristianoc
Copy link
Collaborator Author

Polymorphic variants can be specified with quotes.

@cristianoc
Copy link
Collaborator Author

I can't remember anything having changed in polymorphic variants in v11.
In general, customising polymorphic variants is more difficult because type inference would lose all customisations.
But language-level things such as #42 and #\"AAAA" are preserved.

@cristianoc
Copy link
Collaborator Author

I'll move this to v12, and probably beyond, in case it makes sense to revisit when tooling around bundling changes.

@cristianoc cristianoc changed the title GenType: explore the idea of using namespaces to represent modules. [NotForNow] GenType: explore the idea of using namespaces to represent modules. Apr 12, 2023
@cristianoc cristianoc modified the milestones: v11.0, v12 Apr 12, 2023
@cometkim
Copy link
Member

What about the opportunity of generating .d.ts files? I assume it would simplify a lot of things. But it's just an assumption.

According to this comment, I'm drawing a version of the gentype that only handles types (.d.ts files). It'll bring many advantages and is enough for my own use cases.

I wonder why did gentype include the runtime in the first place?

@cristianoc
Copy link
Collaborator Author

cristianoc commented Apr 20, 2023

It included runtime as the runtime representation required conversion.
E.g. variants would map to incomprehensible numbers on the JS side.

There's still a bit of runtime for @genType.import but that can be looked at separately.

@cometkim
Copy link
Member

ES proposal "module declarations" (currently stage 2) could be more proper target

https://github.com/tc39/proposal-module-declarations

Copy link

github-actions bot commented Dec 9, 2024

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@github-actions github-actions bot added the stale Old issues that went stale label Dec 9, 2024
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Dec 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
experiment stale Old issues that went stale
Projects
None yet
Development

No branches or pull requests

2 participants