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

Proposal do-notation transformer #692

Closed
wernerdegroot opened this issue Jan 21, 2019 · 2 comments
Closed

Proposal do-notation transformer #692

wernerdegroot opened this issue Jan 21, 2019 · 2 comments

Comments

@wernerdegroot
Copy link

This issue is not really an issue, but a request for feedback on something I'm working on.

I am working on a Typescript transformer that will transform something like

function* doStuffWithPromises() {
  const num = yield numberPromise
  const str = yield stringPromise
  return str.length === num
}

to something like

function doStuffWithPromises(): Promise<boolean> {
  return numberPromise.then(num => {
    return stringPromise.then(str => {
      return str.length === num
    })
  })
}

Transforming

The Typescript compiler supports transforms. A transform typically visits every node in your Typescript program (which are a lot of nodes!) and lets you replace/transform the nodes that match a certain condition.

To know when the compiler should transform a generator function to it's nested then-equivalent, we'll let the user wrap the generator function in another function, blabla like so:

const result = blabla<boolean>(function* () {
  const num = yield numberPromise
  const str = yield stringPromise
  return Promise.resolve(str.length === num)
})

The type parameter (boolean in the example above) refers to the result type of the generator function. In this case, result will have the type Promise<boolean>.

The function blabla will be declared in a .d.ts-file, but it won't have a definition. Instead, the transform will see the call to blabla and will take that as the sign to start transforming. The end result will not contain any call to blabla anymore.

The transform will replace every statement like

const myValue = yield myPromise

with

return myPromise.then(myValue => {
  // ...
})

Whatever comes after the original statement will be moved inside the body of the anonymous function within myPromise.then(..). This way, that code has access to myValue and whatever follows should compile just fine.

The transform will continue by doing the exact same thing (replacing a yield with a corresponding call to the then-method) with whatever follows the original statement.

For other monads

That's brilliant, but so far it only works for a Promise, and for Promises we have the perfectly usable async and await syntax. The trick described above is not limited to Promises though. It should also work for Arrays (where we might use Lodash' flatMap) and Observables (where we might prefer mergeMap). To make it work on all these data types, we'll let the user pass the type class that lets us chain these values together. We'll extend our definition of blabla to something you can use as follows:

const result = blabla<'promise', boolean>(promiseMonad, function* () {
  const num = yield numberPromise
  const str = yield stringPromise
  return Promise.resolve(str.length === num)
})

The second type parameter of blabla (boolean in the example above) is similar to the first (and only) parameter in the previous definition of blabla. The first type parameter refers to the URI type parameter as described in the documentation.

This is more or less type-safe already. When you pass 'promise' as the first argument to blabla, but you're yielding an Array, the compiler will complain.

But... As discussed, the result of a yield is always any so that's still a challenge to solve.

Challenges

As the documentation says:

Transforms, all of them, happen after the checking phase has happened

That means, that our transformed generator function will not be type-checked yet. We could still make a mistake!

At this point, I can think of a number of options:

  • I can implement a library in which you can type-check the transformed generator function as a separate compilation step that you can run just like you could run your tests or a linter.
  • I can switch to something like:
const result = blabla<'promise', boolean>(promiseMonad, function(yield_: <A>(pa: Promise<A>) => A) {
  const num = yield_(numberPromise)
  const str = yield_(stringPromise)
  return Promise.resolve(str.length === num)
})

The danger of this is that the yield_ will be erased, like blabla. A user that's unaware of this can let the yield_-function escape from the function. It should also be noted that the yield_ cannot be used outside of the direct function scope of our "generator"-function (something that is automatically enfored with yield):

const result = blabla<'promise', boolean>(promiseMonad, function(yield_: <A>(pa: Promise<A>) => A) {
  function getNumber(): number {
    return yield_(numberPromise)
  }
  const num = getNumber()
  const str = yield_(stringPromise)
  return Promise.resolve(str.length === num)
})

My questions

  • Do you think this is promising? (No pun intended)
  • Which of the two alternatives (separate type-check-step or the approach with yield_) do you prefer?
  • Can you think of other alternatives?
@pfgray
Copy link
Contributor

pfgray commented May 22, 2019

@wernerdegroot , FWIW, there's something like this provided by Do in fp-ts-contrib. This version, while certainly not as succinct or clean as the specialized syntax you're proposing, works with current TS/JS syntax.

Your generator example would look like:

const result = Do(promiseMonad)  // <-- typeclass instance passed here 
  .bindL('num', () => numberPromise)
  .bindL('str', () => stringPromise)
  .return(({num, str}) => str.length === num)

Of course, this works with any type for which a monad instance exists.

Also, the type inference in Do is quite strong. Notice how in the example, no type parameters are provided.

@cyberixae
Copy link
Contributor

@gcanti gcanti closed this as completed Jan 10, 2020
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

4 participants