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

Revisit the design of bindings. #6211

Closed
2 of 3 tasks
cristianoc opened this issue Apr 26, 2023 · 6 comments
Closed
2 of 3 tasks

Revisit the design of bindings. #6211

cristianoc opened this issue Apr 26, 2023 · 6 comments
Labels
stale Old issues that went stale
Milestone

Comments

@cristianoc
Copy link
Collaborator

cristianoc commented Apr 26, 2023

Bindings are one of the most complicated parts of the language, which require understanding a dedicated domain specific language of annotations.
There is also the question of how to generate bindings automatically. This in turn raises the question of expressivity, as not all the TypeScript type language maps nicely to ReScript types.
Finally there's the question of whether one should "trust" the types declared in bindings, or check them. (genType has been experimenting with generating some code to be checked by TS in order to verify consistency).

One of the recent trends is to only bind the parts necessary for a specific project, rather than writing complete bindings for a library.
There's also the frequent suggestion from community members to take existing bindings for a project, and adapt them to your needs when required, rather than trying to have a big blessed repository of bindings.

In the spirit of recent trends, the goal of this issue is to explore just "using the language" for writing bindings, instead of relying on a custom domain specific language of annotations.
The result would be a bit more verbose, but potentially could lower the learning curve for writing bindings.
In particular, the idea is that a user not familiar with ReScript should be able to read existing bindings and immediately understand what they mean, and modify them.
One special such user is AI, so that one goal is to make it as easy as possible for bindings to be generated automatically.

Here are some specific thoughts about one possible step in that direction: https://gist.github.com/cristianoc/00e760e1d5605ddc36fba29d1b1f14c3

Some related issues:

@cristianoc
Copy link
Collaborator Author

An example from the field: https://github.com/cca-io/rescript-material-ui/blob/mui-v5/packages/rescript-mui-material/src/components/Accordion.res

This illustrates the idea that one can often create natural bindings with zero magic -- just use the language.

CC @fhammerschmidt who provided this

@zth
Copy link
Collaborator

zth commented May 28, 2023

Played around with %ffi some. Also chatted to @cristianoc. Here are some thoughts on inlining. Before reading this, please note these are loose thoughts and I have absolutely zero idea if this is even possible or what the implications of implementing this would be. So, just exploring at this point.

What if %ffi have its function body inlined? Examples:

Inline ffi to use es6 syntax from userland

module Array = {
  let concat: (
    array<'value>,
    array<'value>,
  ) => array<'value> = %ffi(`(arr1, arr2) => [...arr1, ...arr2]`)
}

let array = Array.concat([1, 2], [3, 4])

This currently generates:

var concat = ((arr1, arr2) => [...arr1, ...arr2]);

var $$Array = {
  concat: concat
};

var array$1 = concat([
      1,
      2
    ], [
      3,
      4
    ]);

But what if the function body of %ffi could be inlined, and this could instead generate:

var array$1 = [...[1, 2], ...[3, 4]]

Notice it's using es6 spread syntax, but fully controlled via the definition of %ffi.

Essentially how a regular function works today, when ReScript sees it can be inlined, but exclusively for %ffi. This would mean that writing bindings with %ffi could produce clean JS.

Inlining to produce clean JS

Another example:

type element
type window

@val external window: window = "window"

let getActiveElement: window => option<element> = %ffi(`w => w.activeElement`)

let activeElement = window->getActiveElement

This now generates:

var getActiveElement = (w => w.activeElement);

var activeElement = getActiveElement(window);

But if %ffi was inlined, it'd produce:

var activeElement = window.activeElement;

...which would be idiomatic JS.

No idea how far one could take this or if it even makes sense, but it does open up some interesting ideas.

Inlining to do for of and iterators

for of syntax isn't available in ReScript, and is fairly widely used with iterators. Iterators also aren't possible to define directly in ReScript without some tricks, because of them requiring a Symbol property on an object.

However, consider this example of how an iterator + helpers could be defined with %ffi:

module Iterator = {
  type t<'value>

  type iteratorReturn<'value> = {
    done: bool,
    value: option<'value>,
  }

  let make: (
    'self,
    'self => iteratorReturn<'value>,
  ) => t<'value> = %ffi(`(initialValue, nextFn) => {
  return {
    ...initialValue,
    [Symbol.iterator]: function () {
      let self = this
      return {
        next: function () {
          return nextFn(self);
        }
      }
    }
  }
}`)

  let forOf: (t<'value>, 'value => 'return) => unit = %ffi(`(iterator, fn) => {
    for (const v of iterator) {
      fn(v)
    }
}`)

  let toArray: t<'value> => array<'value> = %ffi(`iterator => [...iterator]`)
}

We could then use this to define and use an iterator:

let array = ["item1", "item2", "item3", "item4", "item5"]

type arrayIterable = {
  array: array<string>,
  mutable currentIndex: int,
}

let arrayIterator = Iterator.make(
  {
    array,
    currentIndex: array->Array.length - 1,
  },
  self => {
    if self.currentIndex < 0 {
      {done: true, value: None}
    } else {
      let currentIndex = self.currentIndex
      self.currentIndex = self.currentIndex - 1
      {done: false, value: self.array[currentIndex]}
    }
  },
)

arrayIterator->Iterator.forOf(v => Console.log(v))

let arrayIteratorAsArray = arrayIterator->Iterator.toArray

Today, this produces the following JS:

var make = (initialValue, nextFn) => {
  return {
    ...initialValue,
    [Symbol.iterator]: function () {
      let self = this;
      return {
        next: function () {
          return nextFn(self);
        },
      };
    },
  };
};

var forOf = (iterator, fn) => {
  for (const v of iterator) {
    fn(v);
  }
};

var toArray = (iterator) => [...iterator];

var Iterator = {
  make: make,
  forOf: forOf,
  toArray: toArray,
};

var array = ["item1", "item2", "item3", "item4", "item5"];

var arrayIterator = make(
  {
    array: array,
    currentIndex: (array.length - 1) | 0,
  },
  function (self) {
    if (self.currentIndex < 0) {
      return {
        done: true,
        value: undefined,
      };
    }
    var currentIndex = self.currentIndex;
    self.currentIndex = (self.currentIndex - 1) | 0;
    return {
      done: false,
      value: self.array[currentIndex],
    };
  }
);

forOf(arrayIterator, function (v) {
  console.log(v);
});

var arrayIteratorAsArray = toArray(arrayIterator);

But again, what if %ffi could be inlined. It could potentially produce JS along these lines:

var array = ["item1", "item2", "item3", "item4", "item5"];

var initialValue = {
  array: array,
  currentIndex: (array.length - 1) | 0,
};

var nextFn = function (self) {
  if (self.currentIndex < 0) {
    return {
      done: true,
      value: undefined,
    };
  }
  var currentIndex = self.currentIndex;
  self.currentIndex = (self.currentIndex - 1) | 0;
  return {
    done: false,
    value: self.array[currentIndex],
  };
};

var arrayIterator = {
  ...initialValue,
  [Symbol.iterator]: function () {
    let self = this;
    return {
      next: function () {
        return nextFn(self);
      },
    };
  },
};

var fn = function (v) {
  console.log(v);
};

for (const v of arrayIterator) {
  fn(v);
}

var arrayIteratorAsArray = [...arrayIterator];

I'm sure I'm glossing over a bunch of fantastically difficult technical things, but let a man dream a little 😄

@cristianoc
Copy link
Collaborator Author

Technically just 2 things to be careful with:

  1. don't capture variables on inlining
  2. check for side effects so they're not Eg duplicated

@zth
Copy link
Collaborator

zth commented May 28, 2023

Technically just 2 things to be careful with:

  1. don't capture variables on inlining
  2. check for side effects so they're not Eg duplicated

Looking at it again, one of my examples have inlining inside of the ffi itself. Array.concat inlines both of the arrays into the spread, which is inside the ffi. I guess that's what we'd need to opt out from, inlining inside of the ffi itself. Injecting JS produced by the compiler into the ffi code itself seems unlikely to be viable (although it'd be fantastically cool).

Maybe this is what you meant with your response too.

@cristianoc
Copy link
Collaborator Author

cristianoc commented Jun 1, 2023

Technically just 2 things to be careful with:

  1. don't capture variables on inlining
  2. check for side effects so they're not Eg duplicated

Looking at it again, one of my examples have inlining inside of the ffi itself. Array.concat inlines both of the arrays into the spread, which is inside the ffi. I guess that's what we'd need to opt out from, inlining inside of the ffi itself. Injecting JS produced by the compiler into the ffi code itself seems unlikely to be viable (although it'd be fantastically cool).

Maybe this is what you meant with your response too.

The inlining from your example suffers from these potential issues:

  • capturing variables: when foo(e) injects some code for e which might contain variables which might be captured in the body of foo.
  • the code for e could have side effects and the input variable off foo could be e..g used 0 or 2 times in the body of foo.

Copy link

github-actions bot commented Sep 8, 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 Sep 8, 2024
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Sep 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale Old issues that went stale
Projects
None yet
Development

No branches or pull requests

2 participants