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

Generating methods #3

Open
nojaf opened this issue Nov 2, 2024 · 1 comment
Open

Generating methods #3

nojaf opened this issue Nov 2, 2024 · 1 comment

Comments

@nojaf
Copy link
Collaborator

nojaf commented Nov 2, 2024

As mentioned in #2 (comment), I'm looking into generating methods via the typescript tool.

The first example I'm trying to implement is already an interesting case: https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/connect

Naively it can be added as:

type rec audioNode = {
  /**
    [Read more on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode/numberOfInputs)
    */
  numberOfInputs: any,
  /**
    [Read more on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode/numberOfOutputs)
    */
  numberOfOutputs: any,
  /**
    [Read more on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode/channelCount)
    */
  mutable channelCount: any,

  // this does work though
  connect: (~destinationNode: audioNode, ~output: int=?, ~input: int=?) => unit,
}

connect has overloads. In this case, I'm lucky and the overloads are all optional parameters.
But this might not always be the case.

In case of https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
the last parameter is different and thus the optional types don't work there.

type any

type listener

type eventTarget = {
  addEventListener: (
    ~type_: string,
    ~listener: listener,
    ~options: any=?,
    ~useCapture: bool=?,
  ) => unit,
}

let et: eventTarget = %todo
let callback: listener = %todo

et.addEventListener(~type_="click", ~listener=callback, ~useCapture = true)

generates:

et.addEventListener("click", callback, undefined, true);

so optional arguments won't always work.

It is also not possible to define overloads inside the record using the @as annotation:

type rec audioNode = {
  connect: (~destinationNode: audioNode, ~output: int=?, ~input: int=?) => unit,
  @as("connect") connectWithOutput: (audioNode, int) => unit,
}

// The field connect is defined several times in the record audioNode. Fields can only be added once to a record.

Using a nested module and @send seems like a good trade-off in this case:

/**
A generic interface for representing an audio processing module. Examples include:
[See AudioNode on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode)
*/
type rec audioNode = {
   ...eventTarget,
  /**
    [Read more on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode/numberOfInputs)
    */
  numberOfInputs: any,
  /**
    [Read more on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode/numberOfOutputs)
    */
  numberOfOutputs: any,
  /**
    [Read more on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode/channelCount)
    */
  mutable channelCount: any,
}

module AudioNode = {
  @send external connect: (audioNode, audioNode) => unit = "connect"

  @send
  external connectWithOutput: (audioNode, audioNode, any) => unit = "connect"

  @send
  external connectWithOutputAndInput: (audioNode, audioNode, any, any) => unit =
    "connect"
}

let a: audioNode = %todo
let b: audioNode = %todo
let c: any = %todo

a->AudioNode.connectWithOutput(b, c)

Another thought I have about having dedicated modules for these functions is that we can duplicate the inherited functions and thus avoiding the need to cast it first to a base interface.

type any

type listener

type eventTarget = {}

module EventTarget = {
  @send
  external addEventListener: (eventTarget, string, listener) => unit =
    "addEventListener"
}

/**
A generic interface for representing an audio processing module. Examples include:
[See AudioNode on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode)
*/
type rec audioNode = {
  ...eventTarget,
  /**
    [Read more on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode/numberOfInputs)
    */
  numberOfInputs: any,
  /**
    [Read more on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode/numberOfOutputs)
    */
  numberOfOutputs: any,
  /**
    [Read more on MDN](https://developer.mozilla.org/docs/Web/API/AudioNode/channelCount)
    */
  mutable channelCount: any,
}

module AudioNode = {
  // there will always be scenarios where you want to convert
  external asEventTarget: audioNode => eventTarget = "%identity"

  @send
  external addEventListener: (audioNode, string, listener) => unit =
    "addEventListener"
}

let a: audioNode = %todo
let callback: listener = %todo

a->AudioNode.addEventListener("click", callback)

This again helps with the discoverability if we can nail the trick of showing the completion of addEventListener (from the AudioNode module) after pressing dot in the editor.

Curious to hear your feedback!

@zth
Copy link

zth commented Nov 5, 2024

It is also not possible to define overloads inside the record using the @as annotation:

This is interesting and something that has come up before. When records are used as a binding primitive for handling external data, it would kind of make sense to allow multiple fields with different types pointing to the same underlying field, and just let the developer be "on their own" in terms of if that causes problems at runtime. Just like how bindings work today in general, ie you're responsible of binding the correct thing, and with a few notable exceptions like untagged variants, the runtime type won't be checked.

I wonder what consequence it'd have to allow that in some form though. Coercion becomes a problem obviously, so maybe coercion shouldn't be allowed at all if the record were to have multiple @as for the same field. Curious to hear more thoughts.

Allowing that in some form would solve a lot of problems though... It'd allow us to use records for a larger amount of bindings, and not have to resort to @send external as often.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants