rescript-lang / rescript-compiler

The compiler for ReScript.
https://rescript-lang.org
Other
6.58k stars 438 forks source link

Revisit the design of bindings. #6211

Open cristianoc opened 1 year ago

cristianoc commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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: