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

Allow type arguments to be tuple unpacked #429

Closed
lewis6991 opened this issue Mar 23, 2021 · 18 comments
Closed

Allow type arguments to be tuple unpacked #429

lewis6991 opened this issue Mar 23, 2021 · 18 comments
Labels
feature request New feature or request

Comments

@lewis6991
Copy link

lewis6991 commented Mar 23, 2021

Context

I currently use teal for my neovim plugin and I'm trying to better annotate a small async library I've implemented as part of the plugin which uses lua co-routines.

There is also a more standard neovim async lib in development (in lua) which I would like to port (or at least provide type annotations) in teal.

Issue

Here's a relatively sanitized snippet of what I'm roughly trying to do. Basically I have a type argument T that I would like to be unpacked to represent the argument list of a function.

local type cb_function = function<T>(function(T)) 
-- Here I need T to be tuple unpacked so:
--   cb_function<{string, boolean}> === function(function(string, boolean))

local type async_function = function<T>(...: any): cb_function<T>
-- This is ok

function await<T>(defer: async_function<T>, ...: any): T
  return co.yield(defer(...)) as T
end
-- This is ok (I think)

local file_info: async_function<{string, boolean}> = function(file: string): cb_function<{string, boolean}>
  return function(callback: function(string, boolean))
    local relpath: string
    local is_file: boolean
    ...
    callback(relpath, is_file)
  end
end
-- Also ok

local relpath: string
local is_file: boolean
relpath, is_file = await(file_info, file)
-- The T unpacking above would allow this statement to be properly type checked.

Is this something that can be supported? My only workaround for the time being is to use arrays and varargs:

function M.await<T>(defer: async_function<T>, ...: any): T...
  return co.yield(defer(...)) as T
end

local file_info: async_function<string> = function(file: string): cb_function<string>
  return function(callback: function(...: string))
    local relpath: string
    local is_file: boolean
    ...
    callback(relpath, is_file)
  end
end

local relpath: string
local is_file: boolean
relpath, is_file = await(file_info, file) as (string, boolean)

Many thanks!

@euclidianAce
Copy link
Member

euclidianAce commented Mar 23, 2021

This is something that I would like as well but generics and the mechanisms around them have to be considered carefully. Personally my idea was allowing for variadic typeargs and having some way of selecting them like

local function foo<T...>(x: function( (select(T, 1)) )): select(T, 2)
end

honestly I don't think the implementation would be too hard (but don't quote me on that), I think the real issue is the ergonomics of the syntax.

Perhaps one solution is to allow type-tuples in generics? This would allow for annotation of arguments and returns in generic arguments

local type Foo = function<T, K>(function(T): K): K
local f: Foo<(number, string), (number)>

Both of these are nice for when you want to just forward argument and return values for functions

But it gets more complex with things like varargs, and just regular generics

local type Foo = function<T>(...: T)
local fn: Foo<(number, string)>

local type Bar = function<T>(T)
local b: Bar<(number, string)> -- this definitely shouldn't be allowed

should this be allowed? If it is should the arity of fn now be 2?

Or maybe the solution is something completely different. But I definitely think forwarding function arguments and returns is a common enough use case that at least some subset of it should be supported by the type system

(also #211 is a slightly related but I think different issue)

@euclidianAce euclidianAce added the feature request New feature or request label Mar 23, 2021
@lewis6991
Copy link
Author

lewis6991 commented Mar 23, 2021

local type Foo = function<T>(...: T)
local fn: Foo<(number, string)>

local type Bar = function<T>(T)
local b: Bar<(number, string)> -- this definitely shouldn't be allowed

should this be allowed? If it is should the arity of fn now be 2?

I would have thought the arity of b is 2 and fn would be disallowed.

T -> (number, string), so ...: T doesn't have a sane interpretation since varargs means any number of arguments.

Also consider this (legal) lua snippet:

local function foo(a,b,c)
  return a + b + c
end

local function getargs()
  return 1,2,3
end

print(foo(getargs()))

Which prints 6.

Even though the arity of foo is 3 we can still call it with a single argument if the expression has type tuple3.

With teal generics this could look like:

local function foo(a: number, b: number, c: number): number
  return a + b + c
end

local function getargs(): number, number, number
  return 1,2,3
end

local function apply<T, R>(a: (function(T): R), b: (function(): T)): R
    return a(b())
end

apply(foo, getargs)  -- T -> (number, number, number), and R -> number

@overcrook
Copy link

Lua does not always pass all the function results as-is. If the function call is not the last one in the list of expressions, Lua adjusts the number of returns to 1.

For instance,

local function foo()
   return 1, 2, 3
end

print(foo())
-- >>> 1  2  3

print(foo(), 4)
-- >>> 1  4

See https://www.lua.org/pil/5.1.html for reference.

@hishamhm
Copy link
Member

@overcrook correct, and Teal only expands the tuple of results if it's the last argument, otherwise it truncates it to arity 1, reproducing the expected Lua behavior.

@hishamhm
Copy link
Member

hishamhm commented Mar 29, 2021

This is ugly, but it works:

local async = {
   -- dynamic implementation of await
   await = function(defer: function(...: any): (any), ...:any): any...
      return coroutine.yield(defer(...))
   end
}

local record Async
   type cb_function_1 = function<A>(function(A))
   type cb_function_2 = function<A,B>(function(A,B))
   type cb_function_3 = function<A,B,C>(function(A,B,C))
   type cb_function_4 = function<A,B,C,D>(function(A,B,C,D))
   type cb_function_5 = function<A,B,C,D,E>(function(A,B,C,D,E))
   type cb_function_6 = function<A,B,C,D,E,F>(function(A,B,C,D,E,F))
   type cb_function_7 = function<A,B,C,D,E,F,G>(function(A,B,C,D,E,F,G))
   type cb_function_8 = function<A,B,C,D,E,F,G,H>(function(A,B,C,D,E,F,G,H))
   type cb_function_9 = function<A,B,C,D,E,F,G,H,I>(function(A,B,C,D,E,F,G,H,I))
   type cb_function_10 = function<A,B,C,D,E,F,G,H,I,J>(function(A,B,C,D,E,F,G,H,I,J))

   type async_function_1 = function<A>(...: any): cb_function_1<A>
   type async_function_2 = function<A,B>(...: any): cb_function_2<A,B>
   type async_function_3 = function<A,B,C>(...: any): cb_function_3<A,B,C>
   type async_function_4 = function<A,B,C,D>(...: any): cb_function_4<A,B,C,D>
   type async_function_5 = function<A,B,C,D,E>(...: any): cb_function_5<A,B,C,D,E>
   type async_function_6 = function<A,B,C,D,E,F>(...: any): cb_function_6<A,B,C,D,E,F>
   type async_function_7 = function<A,B,C,D,E,F,G>(...: any): cb_function_7<A,B,C,D,E,F,G>
   type async_function_8 = function<A,B,C,D,E,F,G,H>(...: any): cb_function_8<A,B,C,D,E,F,G,H>
   type async_function_9 = function<A,B,C,D,E,F,G,H,I>(...: any): cb_function_9<A,B,C,D,E,F,G,H,I>
   type async_function_10 = function<A,B,C,D,E,F,G,H,I,J>(...: any): cb_function_10<A,B,C,D,E,F,G,H,I,J>

   -- polymorphic definition of await
   await: function<A>(defer: async_function_1<A>, ...: any): A
   await: function<A,B>(defer: async_function_2<A,B>, ...: any): A,B
   await: function<A,B,C>(defer: async_function_3<A,B,C>, ...: any): A,B,C
   await: function<A,B,C,D>(defer: async_function_4<A,B,C,D>, ...: any): A,B,C,D
   await: function<A,B,C,D,E>(defer: async_function_5<A,B,C,D,E>, ...: any): A,B,C,D,E
   await: function<A,B,C,D,E,F>(defer: async_function_6<A,B,C,D,E,F>, ...: any): A,B,C,D,E,F
   await: function<A,B,C,D,E,F,G>(defer: async_function_7<A,B,C,D,E,F,G>, ...: any): A,B,C,D,E,F,G
   await: function<A,B,C,D,E,F,G,H>(defer: async_function_8<A,B,C,D,E,F,G,H>, ...: any): A,B,C,D,E,F,G,H
   await: function<A,B,C,D,E,F,G,H,I>(defer: async_function_9<A,B,C,D,E,F,G,H,I>, ...: any): A,B,C,D,E,F,G,H,I
   await: function<A,B,C,D,E,F,G,H,I,J>(defer: async_function_10<A,B,C,D,E,F,G,H,I,J>, ...: any): A,B,C,D,E,F,G,H,I,J
end

return async as Async

Yes, it looks like a terrible hack, but there is actually precedent for solutions like this in other languages.

In usage:

local async = require("async")

local function f(n: number): async.cb_function_2<boolean, string>
   return function(): boolean, string
      return true, "hello"
   end
end

local ok, msg = async.await(f, 10)

if ok then
   print(msg)
end

@lewis6991
Copy link
Author

I knew that link would point to the scala docs! Scala also defines all versions of Tuple this way: Tuple2[], Tuple3[], Tuple4[], but for the most part this is more of a backend implementation and the frontend of scalac will actually substitute (T1, T2) -> Tuple2[T1, T2], (T1, T2, T3) -> Tuple3[T1, T2, T3] so even though there is precedent, it's not really user facing as the compiler does desugaring.

Thanks for the solution anyhow, it's certainly usuable.

Would doing a non-hacky solution here by unpacking tuples (as lua does) be really that hard to implement? I.e. function(T) where T is a tuple (S1, S2) becomes function(S1, S2)...but function(T, S3) becomes function(S1, S3)?

@hishamhm
Copy link
Member

(To avoid confusion, what we're calling "tuples" here is what @euclidianAce called type-tuples above, declared as (T1, T2, T3), to distinguish from tuple-arrays, which are Teal tuples declared as {T1, T2, T3} and are Lua tables implementing heterogeneous arrays of fixed size)

Tuples are a second-class type in Lua. It is the type of ..., and there are specific rules about what to do with it if it's the last argument on a function call (unpack) or not (truncate), but other than that there's a lot of things you can do with other types that you can't do with it (store it in a variable, for instance).

Similarly, they are a second-class type in Teal — it exists only inside the compiler to implement Lua semantics, but it's not presented to the user as a declarable type of its own. You're subject to the same limitations using ..., and the only other additional thing you can do with it is use it in as to cast the results of a multiple-returns-function to a different tuple of types.

Allowing such second-class type to be assigned to a type variable would have rippling effects, because a type variable is treated as a first-class type: you can declare variables with it, pass it around, assign it to any, pretty much do anything you can with any other primitive type. By allowing type variables to be type-tuples would change all of that and require adding exceptions in multiple rules. I'm afraid it would complicate the language significantly and it would be easy to get the corner cases wrong.

If we were to have something like this, I think it would make more sense to add "variadic type variables" as in async_function<T...> which would be a sort of second-class type variable. I haven't fully thought it through and it probably also has corner cases to consider and "is this type variable variadic" tests everywhere in the compiler, but it sounds more well-contained at first. It's not something on my immediate radar though.

@lewis6991
Copy link
Author

Thanks for the thorough explanation. Makes complete sense to me.

I'll use the solution you've suggested in the meantime.

@lewis6991
Copy link
Author

For anyone interested I just stumbled upon this really well written blog post that comprehensively explains the second class nature of lua tuples (aka multivals) that is quite relevant to this ticket.

@lewis6991
Copy link
Author

The neovim async lib has now been merged and I've been able to enable type checking using the suggestion of specialising each kind of invocation. The API is a little different than my first comment but the principle is the same.

In order to also type check arguments to an async function I've had to specialise on those too. This makes the resulting code a magnitude more ugly as we now need to speicalise across two dimensions: the input argument types and the return types.

I needed to speciliase for a 4x4 matrix of these values however heres a version for 2x2:

local a = require('plenary/async_lib/async')

local record M
  type future0 = function(function())
  type future1 = function<A1>(function(A1))
  type future2 = function<A1,A2>(function(A1,A2))

  await0: function(future0): ()
  await1: function<A1>(future1<A1>): A1
  await2: function<A1,A2>(future2<A1,A2>): A1,A2

  type async_fun0_0 = function(): future0
  type async_fun0_1 = function<R1>(): future1<R1>
  type async_fun0_2 = function<R1,R2>(): future2<R1,R2>
  type async_fun1_0 = function<A1>(A1): future0
  type async_fun1_1 = function<A1,R1>(A1): future1<R1>
  type async_fun1_2 = function<A1,R1,R2>(A1): future2<R1,R2>
  type async_fun2_0 = function<A1,A2>(A1,A2): future0
  type async_fun2_1 = function<A1,A2,R1>(A1,A2): future1<R1>
  type async_fun2_2 = function<A1,A2,R1,R2>(A1,A2): future2<R1,R2>

  wrap0_0: function(function(function())): async_fun0
  wrap0_1: function<R1>(function(function(R1))): async_fun0_1<R1>
  wrap0_2: function<R1,R2>(function(function(R1,R2))): async_fun0_2<R1,R2>
  wrap1_0: function<A1>(function(A1,function())): async_fun1<A1>
  wrap1_1: function<A1,R1>(function(A1,function(R1))): async_fun1_1<A1,R1>
  wrap1_2: function<A1,R1,R2>(function(A1,function(R1,R2))): async_fun1_2<A1,R1,R2>
  wrap2:_0 function<A1,A2>(function(A1,A2,function())): async_fun2<A1,A2>
  wrap2_1: function<A1,A2,R1>(function(A1,A2,function(R1))): async_fun2_1<A1,A2,R1>
  wrap2_2: function<A1,A2,R1,R2>(function(A1,A2,function(R1,R2))): async_fun2_2<A1,A2,R1,R2>

  async0_0: function(function(): ()): async_fun0
  async0_1: function<R1>(function(): R1): async_fun0_1<R1>
  async0_2: function<R1,R2>(function(): R1,R2): async_fun0_2<R1,R2>
  async1_0: function<A1>(function(A1): ()): async_fun1<A1>
  async1_1: function<A1,R1>(function(A1): R1): async_fun1_1<A1,R1>
  async1_2: function<A1,R1,R2>(function(A1): R1,R2): async_fun1_2<A1,R1,R2>
  async2_0: function<A1,A2>(function(A1,A2): ()): async_fun2<A1,A2>
  async2_1: function<A1,A2,R1>(function(A1,A2): R1): async_fun2_1<A1,A2,R1>
  async2_2: function<A1,A2,R1,R2>(function(A1,A2): R1,R2): async_fun2_2<A1,A2,R1,R2>

end

M.await0 = a.await as function(M.future0): ()
M.await1 = a.await as function<A1>(M.future1<A1>): A1
M.await2 = a.await as function<A1,A2>(M.future2<A1,A2>): A1,A2

M.wrap0_0 = function(func: function(function())): M.async_fun0
  return a.wrap(func, 1) as M.async_fun0
end
M.wrap0_1 = function<R1>(func: function(function(R1))): M.async_fun0_1<R1>
  return a.wrap(func, 1) as M.async_fun0_1<R1>
end
M.wrap0_2 = function<R1,R2>(func: function(function(R1,R2))): M.async_fun0_2<R1,R2>
  return a.wrap(func, 1) as M.async_fun0_2<R1,R2>
end
M.wrap1_0 = function<A1>(func: function(A1,function())): M.async_fun1<A1>
  return a.wrap(func, 2) as M.async_fun1<A1>
end
M.wrap1_1 = function<A1,R1>(func: function(A1,function(R1))): M.async_fun1_1<A1,R1>
  return a.wrap(func, 2) as M.async_fun1_1<A1,R1>
end
M.wrap1_2 = function<A1,R1,R2>(func: function(A1,function(R1,R2))): M.async_fun1_2<A1,R1,R2>
  return a.wrap(func, 2) as M.async_fun1_2<A1,R1,R2>
end
M.wrap2_0 = function<A1,A2>(func: function(A1,A2,function())): M.async_fun2<A1,A2>
  return a.wrap(func, 3) as M.async_fun2<A1,A2>
end
M.wrap2_1 = function<A1,A2,R1>(func: function(A1,A2,function(R1))): M.async_fun2_1<A1,A2,R1>
  return a.wrap(func, 3) as M.async_fun2_1<A1,A2,R1>
end
M.wrap2_2 = function<A1,A2,R1,R2>(func: function(A1,A2,function(R1,R2))): M.async_fun2_2<A1,A2,R1,R2>
  return a.wrap(func, 3) as M.async_fun2_2<A1,A2,R1,R2>
end

M.async0_0 = a.async as function(function(): ()): M.async_fun0
M.async0_1 = a.async as function<R1>(function(): R1): M.async_fun0_1<R1>
M.async0_2 = a.async as function<R1,R2>(function(): R1,R2): M.async_fun0_2<R1,R2>
M.async1_0 = a.async as function<A1>(function(A1): ()): M.async_fun1<A1>
M.async1_1 = a.async as function<A1,R1>(function(A1): R1): M.async_fun1_1<A1,R1>
M.async1_2 = a.async as function<A1,R1,R2>(function(A1): R1,R2): M.async_fun1_2<A1,R1,R2>
M.async2_0 = a.async as function<A1,A2>(function(A1,A2): ()): M.async_fun2<A1,A2>
M.async2_1 = a.async as function<A1,A2,R1>(function(A1,A2): R1): M.async_fun2_1<A1,A2,R1>
M.async2_2 = a.async as function<A1,A2,R1,R2>(function(A1,A2): R1,R2): M.async_fun2_2<A1,A2,R1,R2>

return M

This was so tedious to write for 4x4 that I had to script it!

There's also quite a lot of repitition in here that I'm not sure how to avoid (if it's even possible).

This basically enables full typechecking with statements like:

local get_repo_info = wrap1_3(function(path: string, callback: function(string,string,string))
   ...
end)

local cwd: string = ...;
local toplevel: string, gitdir: string, abbrev_head: string = await3(get_repo_info(cwd))

With variadic type variables I'd hope we could reduce the above to:

local a = require('plenary/async_lib/async')

local record M
  type future = function<A...>(function(A))

  await: function<A...>(future<A>): A

  type async_fun = function<A...,R...>(A): future<R>

  wrap: function<A...,R...>(function(A,function(R))): async_fun<A,R>
  --                                 ^ would this be possible?

  async: function<A...,R...>(function(A): R): async_fun<A,R>
end

M.await1 = a.await as function<A...>(M.future<A>): A

M.wrap = function<A...,R...>(func: function(A, function(R)), argc: integer): M.async_fun<A,R>
  return a.wrap(func, argc) as M.async_fun<A,R>
end

M.async = a.async as function<A...,R...>(function(A): R): M.async_fun<A,R>

return M

Thoughts? I going to guess some of what I want here won't be achievable.

@lenscas
Copy link
Contributor

lenscas commented Apr 2, 2021

For what it is worth: Rust has the same problem but luckily that has macro's to help, resulting in fun code like

impl_teal_multi_value!();
impl_teal_multi_value!(A);
impl_teal_multi_value!(A B);
impl_teal_multi_value!(A B C);
impl_teal_multi_value!(A B C D);
impl_teal_multi_value!(A B C D E);
impl_teal_multi_value!(A B C D E F);
impl_teal_multi_value!(A B C D E F G);
impl_teal_multi_value!(A B C D E F G H);
impl_teal_multi_value!(A B C D E F G H I);
impl_teal_multi_value!(A B C D E F G H I J);
impl_teal_multi_value!(A B C D E F G H I J K);
impl_teal_multi_value!(A B C D E F G H I J K L);
impl_teal_multi_value!(A B C D E F G H I J K L M);
impl_teal_multi_value!(A B C D E F G H I J K L M N);
impl_teal_multi_value!(A B C D E F G H I J K L M N O);
impl_teal_multi_value!(A B C D E F G H I J K L M N O P);

I'm however less sure on what the best solution is. I can see why teal needs a proper solution more than typescript, due to lua's ability to return multiple values. However at the same time, unpacking generics like this looks wild at first glance. Macro's are technically a sort of solution but, as you can see above, that doesn't look great either and macro's are a rabbit hole on their own as well.

@hishamhm
Copy link
Member

hishamhm commented Apr 3, 2021

@lewis6991 thanks for the further feedback! yes, I have briefly thought "there's going to blow up huge if he tries to use if for both inputs and outputs"...

Have you considered using the polymorphic definitions for the public API? That is, using the same function name for the various signatures (see -- polymorphic definition of await in my previous comment above) instead of await0_0, await0_1, etc? This should at least provide a more comfortable API for users of the library. (If you tried and bumped into different issues, I'm especially interested in that!)

@lenscas yeah, I'm leaning towards "second-class variadic type arguments" as I suggested above as a possible solution that would fit, but right now I see it as less of a priority compared to other things like interfaces. But I'm keeping this on the back of my mind; if these scenarios of libraries wrapping functions generically start to pop up more often, this could bump in priority.

@lewis6991
Copy link
Author

Have you considered using the polymorphic definitions for the public API? That is, using the same function name for the various signatures (see -- polymorphic definition of await in my previous comment above) instead of await0_0, await0_1, etc? This should at least provide a more comfortable API for users of the library. (If you tried and bumped into different issues, I'm especially interested in that!)

I did try this and bumped into issues. It wasn't correctly type checking arguments and returns, it didn't error but It also didn't error when it should. I think argument and return types for each function are being unionized incorrectly, e.g. It seems like:

foo: function(string): number
foo: function(number): string

Was being treated as:

foo: function(string|number): number|string

Which is not what I want as foo('string') does not return a string.

I'm probably over generalising here and the specific issues I had were a bit more nuanced so I'll investigate a bit more.

@lewis6991
Copy link
Author

Ok tried using polymorphic functions again and seems to work a treat without any issues. What I witnissed before must of been an error on my part.

FInal types:

local record Async
  type future  = function             (function)
  type future0 = function             (function())
  type future1 = function<A1>         (function(A1))
  type future2 = function<A1,A2>      (function(A1,A2))
  type future3 = function<A1,A2,A3>   (function(A1,A2,A3))
  type future4 = function<A1,A2,A3,A4>(function(A1,A2,A3,A4))

  type async_fun0   = function                           ()            : future0
  type async_fun0_1 = function <R1>                      ()            : future1<R1>
  type async_fun0_2 = function <R1,R2>                   ()            : future2<R1,R2>
  type async_fun0_3 = function <R1,R2,R3>                ()            : future3<R1,R2,R3>
  type async_fun0_4 = function <R1,R2,R3,R4>             ()            : future4<R1,R2,R3,R4>
  type async_fun1   = function <A1>                      (A1)          : future0
  type async_fun1_1 = function <A1,R1>                   (A1)          : future1<R1>
  type async_fun1_2 = function <A1,R1,R2>                (A1)          : future2<R1,R2>
  type async_fun1_3 = function <A1,R1,R2,R3>             (A1)          : future3<R1,R2,R3>
  type async_fun1_4 = function <A1,R1,R2,R3,R4>          (A1)          : future4<R1,R2,R3,R4>
  type async_fun2   = function <A1,A2>                   (A1,A2)       : future0
  type async_fun2_1 = function <A1,A2,R1>                (A1,A2)       : future1<R1>
  type async_fun2_2 = function <A1,A2,R1,R2>             (A1,A2)       : future2<R1,R2>
  type async_fun2_3 = function <A1,A2,R1,R2,R3>          (A1,A2)       : future3<R1,R2,R3>
  type async_fun2_4 = function <A1,A2,R1,R2,R3,R4>       (A1,A2)       : future4<R1,R2,R3,R4>
  type async_fun3   = function <A1,A2,A3>                (A1,A2,A3)    : future0
  type async_fun3_1 = function <A1,A2,A3,R1>             (A1,A2,A3)    : future1<R1>
  type async_fun3_2 = function <A1,A2,A3,R1,R2>          (A1,A2,A3)    : future2<R1,R2>
  type async_fun3_3 = function <A1,A2,A3,R1,R2,R3>       (A1,A2,A3)    : future3<R1,R2,R3>
  type async_fun3_4 = function <A1,A2,A3,R1,R2,R3,R4>    (A1,A2,A3)    : future4<R1,R2,R3,R4>
  type async_fun4   = function <A1,A2,A3,A4>             (A1,A2,A3,A4) : future0
  type async_fun4_1 = function <A1,A2,A3,A4,R1>          (A1,A2,A3,A4) : future1<R1>
  type async_fun4_2 = function <A1,A2,A3,A4,R1,R2>       (A1,A2,A3,A4) : future2<R1,R2>
  type async_fun4_3 = function <A1,A2,A3,A4,R1,R2,R3>    (A1,A2,A3,A4) : future3<R1,R2,R3>
  type async_fun4_4 = function <A1,A2,A3,A4,R1,R2,R3,R4> (A1,A2,A3,A4) : future4<R1,R2,R3,R4>

  await: function              (future0             ): ()
  await: function<A1>          (future1<A1>         ): A1
  await: function<A1,A2>       (future2<A1,A2>      ): A1,A2
  await: function<A1,A2,A3>    (future3<A1,A2,A3>   ): A1,A2,A3
  await: function<A1,A2,A3,A4> (future4<A1,A2,A3,A4>): A1,A2,A3,A4

  async: function                         (function()            : ()         ): async_fun0
  async: function<R1>                     (function()            : R1         ): async_fun0_1 <R1>
  async: function<R1,R2>                  (function()            : R1,R2      ): async_fun0_2 <R1,R2>
  async: function<R1,R2,R3>               (function()            : R1,R2,R3   ): async_fun0_3 <R1,R2,R3>
  async: function<R1,R2,R3,R4>            (function()            : R1,R2,R3,R4): async_fun0_4 <R1,R2,R3,R4>
  async: function<A1>                     (function(A1)          : ()         ): async_fun1   <A1>
  async: function<A1,R1>                  (function(A1)          : R1         ): async_fun1_1 <A1,R1>
  async: function<A1,R1,R2>               (function(A1)          : R1,R2      ): async_fun1_2 <A1,R1,R2>
  async: function<A1,R1,R2,R3>            (function(A1)          : R1,R2,R3   ): async_fun1_3 <A1,R1,R2,R3>
  async: function<A1,R1,R2,R3,R4>         (function(A1)          : R1,R2,R3,R4): async_fun1_4 <A1,R1,R2,R3,R4>
  async: function<A1,A2>                  (function(A1,A2)       : ()         ): async_fun2   <A1,A2>
  async: function<A1,A2,R1>               (function(A1,A2)       : R1         ): async_fun2_1 <A1,A2,R1>
  async: function<A1,A2,R1,R2>            (function(A1,A2)       : R1,R2      ): async_fun2_2 <A1,A2,R1,R2>
  async: function<A1,A2,R1,R2,R3>         (function(A1,A2)       : R1,R2,R3   ): async_fun2_3 <A1,A2,R1,R2,R3>
  async: function<A1,A2,R1,R2,R3,R4>      (function(A1,A2)       : R1,R2,R3,R4): async_fun2_4 <A1,A2,R1,R2,R3,R4>
  async: function<A1,A2,A3>               (function(A1,A2,A3)    : ()         ): async_fun3   <A1,A2,A3>
  async: function<A1,A2,A3,R1>            (function(A1,A2,A3)    : R1         ): async_fun3_1 <A1,A2,A3,R1>
  async: function<A1,A2,A3,R1,R2>         (function(A1,A2,A3)    : R1,R2      ): async_fun3_2 <A1,A2,A3,R1,R2>
  async: function<A1,A2,A3,R1,R2,R3>      (function(A1,A2,A3)    : R1,R2,R3   ): async_fun3_3 <A1,A2,A3,R1,R2,R3>
  async: function<A1,A2,A3,R1,R2,R3,R4>   (function(A1,A2,A3)    : R1,R2,R3,R4): async_fun3_4 <A1,A2,A3,R1,R2,R3,R4>
  async: function<A1,A2,A3,A4>            (function(A1,A2,A3,A4) : ()         ): async_fun4   <A1,A2,A3,A4>
  async: function<A1,A2,A3,A4,R1>         (function(A1,A2,A3,A4) : R1         ): async_fun4_1 <A1,A2,A3,A4,R1>
  async: function<A1,A2,A3,A4,R1,R2>      (function(A1,A2,A3,A4) : R1,R2      ): async_fun4_2 <A1,A2,A3,A4,R1,R2>
  async: function<A1,A2,A3,A4,R1,R2,R3>   (function(A1,A2,A3,A4) : R1,R2,R3   ): async_fun4_3 <A1,A2,A3,A4,R1,R2,R3>
  async: function<A1,A2,A3,A4,R1,R2,R3,R4>(function(A1,A2,A3,A4) : R1,R2,R3,R4): async_fun4_4 <A1,A2,A3,A4,R1,R2,R3,R4>

  wrap: function                         (function(            function())           , integer): async_fun0
  wrap: function<R1>                     (function(            function(R1))         , integer): async_fun0_1 <R1>
  wrap: function<R1,R2>                  (function(            function(R1,R2))      , integer): async_fun0_2 <R1,R2>
  wrap: function<R1,R2,R3>               (function(            function(R1,R2,R3))   , integer): async_fun0_3 <R1,R2,R3>
  wrap: function<R1,R2,R3,R4>            (function(            function(R1,R2,R3,R4)), integer): async_fun0_4 <R1,R2,R3,R4>
  wrap: function<A1>                     (function(A1,         function())           , integer): async_fun1   <A1>
  wrap: function<A1,R1>                  (function(A1,         function(R1))         , integer): async_fun1_1 <A1,R1>
  wrap: function<A1,R1,R2>               (function(A1,         function(R1,R2))      , integer): async_fun1_2 <A1,R1,R2>
  wrap: function<A1,R1,R2,R3>            (function(A1,         function(R1,R2,R3))   , integer): async_fun1_3 <A1,R1,R2,R3>
  wrap: function<A1,R1,R2,R3,R4>         (function(A1,         function(R1,R2,R3,R4)), integer): async_fun1_4 <A1,R1,R2,R3,R4>
  wrap: function<A1,A2>                  (function(A1,A2,      function())           , integer): async_fun2   <A1,A2>
  wrap: function<A1,A2,R1>               (function(A1,A2,      function(R1))         , integer): async_fun2_1 <A1,A2,R1>
  wrap: function<A1,A2,R1,R2>            (function(A1,A2,      function(R1,R2))      , integer): async_fun2_2 <A1,A2,R1,R2>
  wrap: function<A1,A2,R1,R2,R3>         (function(A1,A2,      function(R1,R2,R3))   , integer): async_fun2_3 <A1,A2,R1,R2,R3>
  wrap: function<A1,A2,R1,R2,R3,R4>      (function(A1,A2,      function(R1,R2,R3,R4)), integer): async_fun2_4 <A1,A2,R1,R2,R3,R4>
  wrap: function<A1,A2,A3>               (function(A1,A2,A3,   function())           , integer): async_fun3   <A1,A2,A3>
  wrap: function<A1,A2,A3,R1>            (function(A1,A2,A3,   function(R1))         , integer): async_fun3_1 <A1,A2,A3,R1>
  wrap: function<A1,A2,A3,R1,R2>         (function(A1,A2,A3,   function(R1,R2))      , integer): async_fun3_2 <A1,A2,A3,R1,R2>
  wrap: function<A1,A2,A3,R1,R2,R3>      (function(A1,A2,A3,   function(R1,R2,R3))   , integer): async_fun3_3 <A1,A2,A3,R1,R2,R3>
  wrap: function<A1,A2,A3,R1,R2,R3,R4>   (function(A1,A2,A3,   function(R1,R2,R3,R4)), integer): async_fun3_4 <A1,A2,A3,R1,R2,R3,R4>
  wrap: function<A1,A2,A3,A4>            (function(A1,A2,A3,A4,function())           , integer): async_fun4   <A1,A2,A3,A4>
  wrap: function<A1,A2,A3,A4,R1>         (function(A1,A2,A3,A4,function(R1))         , integer): async_fun4_1 <A1,A2,A3,A4,R1>
  wrap: function<A1,A2,A3,A4,R1,R2>      (function(A1,A2,A3,A4,function(R1,R2))      , integer): async_fun4_2 <A1,A2,A3,A4,R1,R2>
  wrap: function<A1,A2,A3,A4,R1,R2,R3>   (function(A1,A2,A3,A4,function(R1,R2,R3))   , integer): async_fun4_3 <A1,A2,A3,A4,R1,R2,R3>
  wrap: function<A1,A2,A3,A4,R1,R2,R3,R4>(function(A1,A2,A3,A4,function(R1,R2,R3,R4)), integer): async_fun4_4 <A1,A2,A3,A4,R1,R2,R3,R4>

end

return Async

I can also implement this as a .d.tl so I don't need to wrap the library.

Though still ugly, I'm much happier with this. Thanks!

@lewis6991
Copy link
Author

lewis6991 commented Apr 8, 2021

I've bumped into an issue with polymorphism of the async function. It seems tl isn't distinguishing on the return type of the function argument:

async: function       (function(): ()   ): async_fun0
async: function<R1>   (function(): R1   ): async_fun0_1 <R1>
async: function<R1,R2>(function(): R1,R2): async_fun0_2 <R1,R2>

All appear to be treated the same which results in the error with:

toplevel, gitdir, abbrev_head = await(get_repo_info(file))  
-- only 1 value is returned by this function
-- variable is not being assigned a value

This is unlike wrap which works perfectly as it has the return types encoded into the callback argument.

Is this a design feature (return type erasure) or can we consider this a bug?

@hishamhm
Copy link
Member

hishamhm commented Apr 8, 2021

@lewis6991 Please try reordering the polymorphic definitions from "most specific" to "least specific" and let me know if it solves the issue! The ordering in polymorphic definitions are relevant.

@lewis6991
Copy link
Author

Thanks, that's solved it!

@lewis6991
Copy link
Author

Hmm for wrap it appears to want the definitions ordered from "least specific" to "most specific". Is that expected?

hishamhm added a commit that referenced this issue May 14, 2021
hishamhm added a commit that referenced this issue May 14, 2021
hishamhm added a commit that referenced this issue May 15, 2021
hishamhm added a commit that referenced this issue Jul 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants