Hello! I am trying to create the typechecking for a function that composes a base function with another function n times using the old type solver, but this causes the generics to lock after they have been assigned once.
Implementation
Composer function
The function below takes two parameters, functions: fa
, fb
, and returns function fc
:
local compose: <A,B,C>(
fa: (A) -> (B),
fb: (B) -> (C)
) -> (A) -> (C) --< fc
where:
fa
is being composed.
fb
is composing.
fc
is the result, a function with the parameter of fa
and return type of fb
lifted into fa
’s return context, identical to: fb(fa(A))
To give an example, take functions fna
and fnb
:
local fna: <T>(T) -> (T?)
local fnb: <T>(T) -> ({T})
fna
takes value T
and returns itself or nil: T?
fnb
takes value T
and returns itself in an array: {T}
Expected results
Composing these: fnb(fna(T))
, should return {T?}
because fnb(T?)
= {T?}
Manual results
When composing manually, they return the expected result:
Inputting a string
will return {string?}
Inputting a number
will return {number?}
fnb(fna("")) -> {string?}
fnb(fna(1)) -> {number?}
However; this is not the same for the composer function.
Composer function’s results
local fnc = compose(fna, fnb)
fnc("") -> {string?}
fnc(1) -> {string?}
As soon as fnc is called once, the generic locks to that value, any new return will now have the string
type. Parameters are also affected:
The first call is normal
But the second call’s generics no longer accept new types, it has become locked.
Other posts
Did not find any similar posts after a thorough search, It’s most likely covered under a separate context.
Workarounds
A fully dynamic setup, like the one below, does not work because of generic subtype escaping scope:
local function compose(fa,fb)
return function(value)
return fb(fa(value))
end
end
What you can do is create a sort of factory function that produces the composition every time:
local stored
local function getFnc()
return compose(fna,fnb)
end
local function fnc(value)
if not stored then
stored = getFnc()
end--breaks without casting :: any for some reason
return stored(value::any) :: typeof(getFnc()(value))
end
But frankly this is very tedious and not realistic to do in practice.
There is a much more promising workaround that I’ve found but its implementation is highly complex, enough so to be more of a resource than a simple fix.
Questions
- Is this a bug, limitation, or fault in my own approach?
- Are there any known workarounds that don’t require external setups like the one shown above?