Search code examples
type-safetytype-systems

How are unused returned values handled in typed languages with type inference?


How do strongly typed languages that have type inference, handle the unused returned values (where it's not obvious what the intended type is)?

Here's an example where this happens in the untyped Python, where it's of course not caught:

list(map(lambda item: item.some_method, some_collection))

Here item.some_method was intended to be called, for its side effects, so the correct line is:

list(map(lambda item: item.some_method(), some_collection))

Solution

  • Without additional constraints, a strongly-typed language with type inference will typically not prevent the first example, as it is technically valid, as you point out. There are a few ways this could be avoided.

    1. You could explicitly annotate the type of the map (or use it somewhere that forces the type to be equal to something). This would result in a type checking error if the wrong variant is used.
    2. Some languages (e.g. Rust) distinguish between map and for_each, where the latter is expected to be impure/effectful. If you use for_each in the first example, this would fail to compile, as the type of lambda item: item.some_method is A -> () -> (), rather than A -> (). However, nothing prevents you from unidiomatically using map here.
    3. Some languages (e.g. Haskell) distinguish between impure/effectful values and pure/effectless values. In this setting, map would be expected to take a lambda that is pure (making lambda item: item.some_method() invalid) and for_each would be expected to take a lambda that is impure (making lambda item: item.some_method invalid). This is enforced using monads, which act as a marker that tell the type system a value is impure (e.g. the type IO(A) represents an effectful A type).
    4. Some languages (e.g. Rust) allow the use of values to be tracked and functions to be annotated to enforce their return values to be "used" in some way. If map is annotated #[must_use] in this way, then its return value must be used, which would alert you to an incorrect use of lambda item: item.some_method (as the return value of map would not be used). In Rust, the unit type () is automatically "used", and in theory this could be extended to include [()], in which case the correct code lambda item: item.some_method() would work without issues.