A free variable declared using forall only works if it is used in the method's signature. This allows the compiler to defer a concrete type for each method instantiation.
Currently, this code compiles:
def foo() forall T
end
foo
The same is true when the method has arguments but none of type T.
I think this code should fail, because an explicitly declared free variable is not used and can't be deferred.
When the free variable is used inside the method, it already fails because the constant is not defined, even though it's declared as a free variable.
def foo() forall T
T # Error: undefined constant T
end
foo
This error message is not as clear as when the method signature would already error with a message saying that you can't use a free variable unless it's used in the arguments.
When leaving out the empty parenthesis (which is otherwise valid when there are not arguments), it is already a syntax error:
def foo forall T # Syntax error: unexpected token: forall (parentheses are mandatory for def arguments)
end
foo
When the free variable is used in a block argument, this is also already an error:
def foo(&block : T -> _) forall T # Error: undefined constant T
end
foo {|x| x}
That's because the type of block arguments has no influence on method dispatch and instantiation. Thus free variables can't be deferred from block arguments.
It would be great however, if this would raise a similar and more meaningful error.
This issue came up on the forums in https://forum.crystal-lang.org/t/generics-type-parameter-inherite/724 and I added a few more scenarios.
The original example described a special case when free variables are combined with generic type arguments. Because both share the same namespace (single uppercase letters), a free variable can use the same name as a generic type argument and overrides it in the respective method's scope.
The exact same method as in the second example, but declared inside a generic type, suddenly compiles:
class Foo(T)
def foo() forall T
T
end
end
Foo(Nil).new.foo # => Nil
Since the free variable can't be deferred, T simply references the generic type argument.
About the last example with a block:
def foo(&block : T -> _) forall T # Error: undefined constant T
end
foo {|x| x}
What about if we pass a proc? (didn't tested) It should work since the proc is typed.
The block input argument is used to compute the block output argument, for example to be able to define Enumerable#map or Array#map. So using forall T with a block argument will never work.
@bew it work with a proc.
This https://github.com/alex-lairan/monads/pull/25 resolve the error we got with block by using a proc instead.
It's not pretty, but it do the job.
Following code is valid:
def method1(proc : _ -> _)
end
method1(->(x : Int32) { x })
But, following code isn't valid.
def method2(&block : _ -> _)
end
method2 { |x| x }
I suppose that there is no uniformity.
Using a proc as a regular argument is an entirely different case. A Proc(X, Y) argument is like Hash(X, Y) just a plain generic type.
This has nothing to do with block arguments (incl. captured procs).
Of-course it's not the same.
But the block behavior and the proc behavior are similar in a user point of view. (Not similar for an expert or a core developer)
This is a bit impossible to do with how macros work. Should this compile?
def foo(x) forall T
bar
end
Unfortunately bar could be a macro that uses T so it's really hard to see, syntactically, that T is used.
Plus I think this is very low priority.
@asterite That's exactly my point. Whether forall T compiles shouldn't depend on whether T is used in the method body. It only matters if T is used in the arguments. If not, it should always fail.
I know it's a low priority, but there is any changes or planned change for this issue ? :smile:
This works, for now, I don't know from when, but it works :)
Most helpful comment
@asterite That's exactly my point. Whether
forall Tcompiles shouldn't depend on whetherTis used in the method body. It only matters ifTis used in the arguments. If not, it should always fail.