Composed generic functions become locked on subsequent calls

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

  1. Is this a bug, limitation, or fault in my own approach?
  2. Are there any known workarounds that don’t require external setups like the one shown above?

I think that is because when you store the composed function, the generic type is auto-filled. Since you’ve called fnc(""), the generic type will think the function accepts only strings; consequently, when you subsequently used fnc(2), it will type error.

1 Like

Yeah I believe we’re on the same page.
As I see it, the type solver presumably evaluates generics immediately, but because I’m returning a callable function instead of an immediate value, it just evaluates the expression from the first call instead, though this is hard to verify.

It could also just be the “Generic sub-type escaping scope” but without its error message.

1 Like

Yes, the generic type is auto-filled according to the first parameter type used on the first function call. This happens to a lot of things. When you store a variable (in this case, a function), it’s first not auto-filled, since you didn’t provide any parameters to it. When you provide it with the parameter, the variable now understands that the function type is parameter type, the generic function is auto-filled.

Thus, when you subsequently access the function variable, it will now understand that all supported parameters is the type provided before. I don’t think this is a bug, it’s just how generic type works.

1 Like

Thank you for your input, I will keep this post up for a day or two to get some more perspectives.

It’s fine! I hope you can find a better way to solve it, since even I have some difficulties regarding types and stuff :sweat_smile:

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.