# Type instability in list comprehensions

**URL:** <https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673>\
**Category:** General Usage\
**Tags:** repl, type-stability, comprehension\
**Created:** [July 31, 2024, 12:31pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673 "2024-07-31T12:31:06Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![mwallerb](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mwallerb/32/52642_2.png) [@mwallerb](https://discourse.julialang.org/u/mwallerb)\
**Post date:** [July 31, 2024, 12:31pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/1 "2024-07-31T12:31:07Z")

</div>

Consider the following code:

```
julia> x = [1, 2];
julia> y = Float64[];
julia> [xi * yi for xi in x for yi in y]
Any[]

julia> f(x, y) = [xi * yi for xi in x for yi in y]; f(x, y)
Float64[]

```

so the whole thing is type-unstable. Even more surprisingly, pasting the arguments into the comprehension resolves it:

```
julia> [xi * yi for xi in [1,2] for yi in Float64[]]
Float64[]

```

One way to resolve this is to use `map` together with `product`:

```
julia> vec(map(((xi,yi),) -> xi*yi, Iterators.product(x, y)))
Float64[]

```

which is clunky and annoying. This, to me, seems to be a straight-forward julia **bug** , which caused all sorts of nasty surprises to me. Why does julia behave that way?

_Edited to add_: note that all of these example **are** type-stable if `y` is non-empty:

```
julia> y = [3.0];
julia> [xi * yi for xi in x for yi in y]
4-element Vector{Float64}:
  3.0 
  6.0

```

---

<div class="post-metadata">

**Author:** ![adienes](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/adienes/32/37459_2.png) [@adienes](https://discourse.julialang.org/u/adienes)\
**Post date:** [July 31, 2024, 12:41pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/2 "2024-07-31T12:41:34Z")

</div>

> [@mwallerb](#):
>
> `julia> [xi * yi for xi in x for yi in y]`

in this case, `x` and `y` are globals so you’ll have a hard time finding type stability no matter what you do with them. you can resolve this particular problem by either adding type assertions or making them `const` (or both)

but the “better” solution is to add a function barrier. when you put it inside a function, `x` and `y` are specialized on with a type.

---

<div class="post-metadata">

**Author:** ![pfitzseb](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/pfitzseb/32/45566_2.png) [@pfitzseb](https://discourse.julialang.org/u/pfitzseb)\
**Post date:** [July 31, 2024, 12:43pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/3 "2024-07-31T12:43:11Z")

</div>

Yeah, don’t expect type stability with globals. Or make them `const`:

```julia
julia> const x = [1,2];

julia> const y = Float64[];

julia> [xi * yi for xi in x for yi in y]
Float64[]

```

---

<div class="post-metadata">

**Author:** ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)\
**Post date:** [July 31, 2024, 1:08pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/4 "2024-07-31T13:08:31Z")

</div>

You can also type your ~~list~~ array comprehensions.

```julia
julia> x = [1, 2];

julia> y = Float64[];

julia> [xi * yi for xi in x for yi in y]
Any[]

julia> Float64[xi * yi for xi in x for yi in y]
Float64[]

```

---

<div class="post-metadata">

**Author:** ![mwallerb](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mwallerb/32/52642_2.png) [@mwallerb](https://discourse.julialang.org/u/mwallerb)\
**Post date:** [July 31, 2024, 1:27pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/5 "2024-07-31T13:27:33Z")

</div>

Hang on, but that is not true if x and y are non-empty!

```
julia> x = [1, 2]; y = [3., 4.]
julia> [xi*yi for xi in x for yi in y]
4-element Vector{Float64}:
  3.0
  4.0
  6.0
  8.0

```

So, it only does not work for empty ones! So IMHO type instability of globals does not (fully) explain this…

---

<div class="post-metadata">

**Author:** ![mrufsvold](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mrufsvold/32/31600_2.png) [@mrufsvold](https://discourse.julialang.org/u/mrufsvold)\
**Post date:** [July 31, 2024, 1:30pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/6 "2024-07-31T13:30:16Z")

</div>

If the lists are not empty, then Julia can look at the actual types produced by the function in the comprehension to figure out the eltype of the result vector.

If they are empty, then the compiler has to rely on the Type Domain to compute what the eltype ought to be. So the type instability of globals messes it up!

---

<div class="post-metadata">

**Author:** ![mwallerb](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mwallerb/32/52642_2.png) [@mwallerb](https://discourse.julialang.org/u/mwallerb)\
**Post date:** [July 31, 2024, 1:36pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/7 "2024-07-31T13:36:55Z")

</div>

But if I understand you correctly, that would mean that this example is type-instable anyway, it’s just that the compiler looks at the result type of the function as applied to the first item, guesses that this is going to be the `eltype` of the result vector, and then course-corrects once it sees that this fails? In other words, it has to do dynamical dispatch for every element?

The other thing I still do not understand is why `const` is so important here … at the moment of invocation of the command on the repl, I know the types of these variables, whether they are const or not, and I have to select the current code paths anyway, so why do I need const here?

---

<div class="post-metadata">

**Author:** ![mrufsvold](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mrufsvold/32/31600_2.png) [@mrufsvold](https://discourse.julialang.org/u/mrufsvold)\
**Post date:** [July 31, 2024, 1:40pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/8 "2024-07-31T13:40:31Z")

</div>

My answer is an oversimplification of the fancy stuff the compiler does to try to tack down type stability in a dynamic language. It does not course correct for each iteration, but I don’t fully know how it works.

---

<div class="post-metadata">

**Author:** ![adienes](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/adienes/32/37459_2.png) [@adienes](https://discourse.julialang.org/u/adienes)\
**Post date:** [July 31, 2024, 1:42pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/9 "2024-07-31T13:42:41Z")

</div>

> [@mwallerb](#):
>
> looks at the result type of the function as applied to the first item, guesses that this is going to be the `eltype` of the result vector, and then course-corrects once it sees that this fails? In other words, it has to do dynamical dispatch for every element?

not quite.

`[foo(x) for x in arr if bar(x)]` gets “lowered” to something more like `collect(Base.Iterators.Generator(bar, Base.Iterators.Filter(bar, arr)))` at which point the compiler can and will do all sorts of fancy things more sophisticated than just looking at the first element (actually if `foo` and `bar` are type stable and `arr` is concretely typed, I don’t think it should look at any elements at all. but I’m not an expert here)

---

<div class="post-metadata">

**Author:** ![mrufsvold](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mrufsvold/32/31600_2.png) [@mrufsvold](https://discourse.julialang.org/u/mrufsvold)\
**Post date:** [July 31, 2024, 1:48pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/10 "2024-07-31T13:48:01Z")

</div>

```julia
x = [1,2,3]
y_int = Int[]
y_float = Float[]
y = y_int

# round and round it goes 
# where will it stop? Nobody knows!
@spawn for i in 1:1000
    y = (y_int, y_float)[i%2+1]
    sleep(0.001)
end

# between compile time and run time of this function, the type of y may change
[xi * yi for xi in x for yi in y]

```

---

<div class="post-metadata">

**Author:** ![mohamed.d180](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mohamed.d180/32/52028_2.png) [@mohamed.d180](https://discourse.julialang.org/u/mohamed.d180)\
**Post date:** [July 31, 2024, 4:43pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/11 "2024-07-31T16:43:22Z")

</div>

I think it is all about type instabilities of using global variables inside functions, means without passing it as arguments.

global variables can change its type at any time to any thing.

Try this code and you will see that the compiler can’t handle the type the elements in the empty Float array

```julia
julia> x = Float64[]
Float64[]

julia> function g()
           for i in x
               println(typeof(i))
           end
       end
g (generic function with 1 method)

julia> @code_warntype g()
MethodInstance for g()
  from g() @ Main REPL[2]:1
Arguments
  #self#::Core.Const(g)
Locals
  @_2::Any
  i::Any # <--------------- See here
Body::Nothing
1 ─ %1 = Main.x::Any
│ (@_2 = Base.iterate(%1))
│ %3 = (@_2 === nothing)::Bool
│ %4 = Base.not_int(%3)::Bool
└── goto #4 if not %4
2 ┄ %6 = @_2::Any
│ (i = Core.getfield(%6, 1))
│ %8 = Core.getfield(%6, 2)::Any
│ %9 = Main.typeof(i)::DataType
│ Main.println(%9)
│ (@_2 = Base.iterate(%1, %8))
│ %12 = (@_2 === nothing)::Bool
│ %13 = Base.not_int(%12)::Bool
└── goto #4 if not %13
3 ─ goto #2
4 ┄ return nothing

```

---

<div class="post-metadata">

**Author:** ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)\
**Post date:** [July 31, 2024, 11:00pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/12 "2024-07-31T23:00:34Z")

</div>

> [@mwallerb](#):
>
> The other thing I still do not understand is why `const` is so important here … at the moment of invocation of the command on the repl, I know the types of these variables, whether they are const or not, and I have to select the current code paths anyway, so why do I need const here?

One main consideration is if the compiler can assume the global variables _and_ their type will not change. Recent Julia versions allow you to bind the types of globals.

```julia-repl
julia> x::Vector{Float64} = [1,2];

julia> y::Vector{Float64} = [];

julia> [xi * yi for xi in x for yi in y]
Float64[]

julia> f() = [xi * yi for xi in x for yi in y]
f (generic function with 1 method)

julia> @code_warntype f()
MethodInstance for f()
  from f() @ Main REPL[3]:1
Arguments
  #self#::Core.Const(f)
Locals
  #2::var"#2#3"
Body::Vector{Float64}
1 ─ (#2 = %new(Main.:(var"#2#3")))
│ %2 = #2::Core.Const(var"#2#3"())
│ %3 = Base.Generator(%2, Main.x)::Base.Generator{Vector{Float64}, var"#2#3"}
│ %4 = Base.Flatten(%3)::Base.Iterators.Flatten{Base.Generator{Vector{Float64}, var"#2#3"}}
│ %5 = Base.collect(%4)::Vector{Float64}
└── return %5

```

Compare the typed output to the following.

```julia-repl
julia> x = [1,2]
2-element Vector{Int64}:
 1
 2

julia> y = Float64[]
Float64[]

julia> f() = [xi * yi for xi in x for yi in y]
f (generic function with 1 method)

julia> @code_warntype f()
MethodInstance for f()
  from f() @ Main REPL[3]:1
Arguments
  #self#::Core.Const(f)
Locals
  #2::var"#2#3"
Body::Vector
1 ─ (#2 = %new(Main.:(var"#2#3")))
│ %2 = #2::Core.Const(var"#2#3"())
│ %3 = Base.Generator(%2, Main.x)::Base.Generator{_A, var"#2#3"} where _A
│ %4 = Base.Flatten(%3)::Base.Iterators.Flatten{I} where I<:(Base.Generator{_A, var"#2#3"} where _A)
│ %5 = Base.collect(%4)::Vector
└── return %5

```

---

<div class="post-metadata">

**Author:** ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)\
**Post date:** [July 31, 2024, 11:34pm UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/13 "2024-07-31T23:34:09Z")

</div>

This looks like an example of compiler optimizations leaking into observable behavior—specifically, an expression whose return value depends on the success of type inference. I agree that this should be considered a straightforward bug.

Type indeterminism might be a more accurate description than type instability for this kind of behavior.

---

<div class="post-metadata">

**Author:** ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)\
**Post date:** [August 1, 2024, 1:58am UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/14 "2024-08-01T01:58:38Z")

</div>

> [@danielwe](#):
>
> This looks like an example of compiler optimizations leaking into observable behavior—specifically, an expression whose return value depends on the success of type inference.

But it doesn’t?

---

<div class="post-metadata">

**Author:** ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)\
**Post date:** [August 1, 2024, 2:03am UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/15 "2024-08-01T02:03:30Z")

</div>

It does? The OP shows that the expression `[xi * yi for xi in x for yi in y]` can return different values (`Any[]` vs `Float64[]`) for the same values of `x` and `y`.

---

<div class="post-metadata">

**Author:** ![adienes](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/adienes/32/37459_2.png) [@adienes](https://discourse.julialang.org/u/adienes)\
**Post date:** [August 1, 2024, 2:26am UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/16 "2024-08-01T02:26:16Z")

</div>

> for the same values of `x` and `y`.

but they are not the same values of `x` (kinda, in the `===` sense that they are distinguishable in valid programs)

in one

```julia
julia> x = [1, 2];

julia> Core.get_binding_type(Main, :x)
Any

```

and the other

```julia
julia> x::Vector{Float64} = [1, 2];

julia> Core.get_binding_type(Main, :x)
Vector{Float64} (alias for Array{Float64, 1})

```

---

<div class="post-metadata">

**Author:** ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)\
**Post date:** [August 1, 2024, 3:11am UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/17 "2024-08-01T03:11:37Z")

</div>

But behavior in Julia is (should be) determined by runtime values, not binding types. This is why multiple dispatch is different from C++ function overloading. It’s certainly possible to write code that violates this rule using internals like `Core.Compiler.return_type`, but it’s strongly discouraged and the creators regret that such functions are exposed at all: [Create `infer_eltype` function by Tortar · Pull Request #54909 · JuliaLang/julia · GitHub](https://github.com/JuliaLang/julia/pull/54909#issuecomment-2234257481)

---

<div class="post-metadata">

**Author:** ![CameronBieganek](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cameronbieganek/32/6915_2.png) [@CameronBieganek](https://discourse.julialang.org/u/CameronBieganek)\
**Post date:** [August 1, 2024, 5:42am UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/18 "2024-08-01T05:42:26Z")

</div>

I agree with @danielwe, although I wouldn’t quite call it a bug since Julia has been this way for a long time, probably since before 1.0. Base Julia does make use of `Core.Compiler.return_type` in some places like in generators and array comprehensions and I think also in broadcasting.\* (I’m not sure if the `map` implementation uses `Core.Compiler.return_type` anywhere…) So we sometimes encounter cases like this where the value of an expression depends on type inference, which is definitely not a good thing.

We would need some different semantics to solve some of these problems. Perhaps the mutate-or-widen approach advocated by `@tkf`.

\*I’m speaking in generalities here. I don’t remember exactly where Base uses `Core.Compiler.return_type`, I just know that I’ve seen it in some of those places when I’ve looked at the code in the past.

---

<div class="post-metadata">

**Author:** ![danielwe](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/danielwe/32/35657_2.png) [@danielwe](https://discourse.julialang.org/u/danielwe)\
**Post date:** [August 1, 2024, 6:42am UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/19 "2024-08-01T06:42:13Z")

</div>

It’s perhaps worth noting that harmless use of `Core.Compiler.return_type` is possible. The rule is to use it for optimizations only and ensure that your function’s return value is the same regardless of what `return_type` gives you.

I too have a hazy memory of discussions relating to `map` over empty iterables and inference-dependent return values, but I thought these were also considered bugs? It’s bad because `return_type` can differ in its ability to infer a particular call not only between Julia versions but even within a single Julia session.

---

<div class="post-metadata">

**Author:** ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)\
**Post date:** [August 1, 2024, 9:49am UTC](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673/20 "2024-08-01T09:49:45Z")

</div>

> [@danielwe](#):
>
> The OP shows that the expression `[xi * yi for xi in x for yi in y]` can return different values (`Any[]` vs `Float64[]`) for the same values of `x` and `y`

In any case, I don’t think this has anything to do with type inference or `Core.Compiler.return_type`, and in particular I don’t think it’d be susceptible to the nonndeterminism of type inference. AFAIK Julia models non-`const` globals as some kind of reference, which is typed for typed globals, and untyped for untyped globals. So, even though the relevant types are not accessible to the user, this is simply a matter of the ouput type depending on the input type, as it’s supposed to.

[Next page](https://discourse.julialang.org/t/type-instability-in-list-comprehensions/117673.md?page=2)
