Map while preserving container type

I thought map was supposed to preserve the type of the container but both map and broadcast produce Arrays here. Why does this happen? What is the recommended way to apply a function to each element of a container while preserving the container type?

julia> gen = (x+10 for x in 1:100000); (-).(gen)
100000-element Array{Int64,1}:
     -11
     -12
...
 -100009
 -100010

julia> gen = (x+10 for x in 1:100000); map(-, gen)
100000-element Array{Int64,1}:
     -11
     -12
...
 -100009
 -100010

Both map and broadcast are eager, not lazy, therefore they iterate the generator to apply - to every element and, consequently, they return a materialization of the result. To apply a function to every element in a generator and have a generator at the end, you need to wrap your generator in another generator:

julia> gen = (x+10 for x in 1:100000); (-x for x in gen)
Base.Generator{Base.Generator{UnitRange{Int64},var"#1#2"},typeof(-)}(-, Base.Generator{UnitRange{Int64},var"#1#2"}(var"#1#2"(), 1:100000))

But notice that, anyway, you will change the exact type of the generator object, because it is parametrized by the range and applied function. But, at least, it is lazy, not eager.

1 Like

Is it possible to write a general map : (x -> y) -> c x -> c y that preserves the container type?

Are these Haskell signatures I see here? XD

If you are talking about a method, i.e., a single body, I am not entirely sure. You could use similar (but I think it also materializes), or capture the container type and call it as a constructor, but the fact is that there is no general interface all collections are forced to follow that allows for creating a new object from an old object of the same type and a function to be applied to every element. If every container you are interested on accepts an iterable of the values as single parameter for the constructor, well, then you can use a generator that applies the function to every element and pass it to the constructor of the type (that you can get with typeof I think). But it may be some container, like Base.ImmutableDict, that does not act like this.

If you are talking about a function, and you are able to add new methods as you need them, well, then the sky is the limit. You can throw an error in the body that receives container :: Any and create a specific body for each type of container you may use.

There is the option of Iterators.map which is lazy instead of the default map which is eager.

julia> gen = (x+10 for x in 1:100000); t = Iterators.map(-, gen);

julia> typeof(gen)
Base.Generator{UnitRange{Int64}, var"#7#8"}

julia> typeof(t)
Base.Generator{Base.Generator{UnitRange{Int64}, var"#7#8"}, typeof(-)}

I think I found it as Kaleido.fmap.

1 Like