AFAIK in Ruby you can do Array#flatten(n), which would flatten only up to n levels:
[1, [2, [3, 4]]].flatten(1)
# => [1, 2, [3, 4]]
I'd like too see such a feature in Crystal. WDYT?
Send a PR ;-)
A word of warning though, getting the type signatures right is not obvious - I know because I submitted a pull request for this exact feature earlier. And searching the history finds more tries :)
I know because I submitted a pull request for this exact feature earlier. And searching the history finds more tries :)
Indeed, #3541 to be exact.
I have been implementing a version of flatten that takes an element type as an argument and flattens as much as needed so that the end result is an array with that element type. The code is available here.
To use this, a person wanting to flatten an array by e.g. one level would have to figure out what is the element type that the resulting array should have, and to use that element type as the argument. For instance, the original example would be written like this:
[1, [2, [3, 4]]].flatten(Int32 | Array(Int32))
# => [1, 2, [3, 4]]
The result type is guaranteed to be an array of the given element type. In the above example it would be Array(Array(Int32) | Int32).
Would this be useful? If so, I can look into turning the code into a PR.
@jbomanson that's an interesting idea, I'd love to see a PR! Just on the API side, I think flatten(to: Array(Array(Int32) | Int32)) would be a more descriptive way of expressing this, if a bit longer.
@jbomanson it's a nice compromise. I think it will have some issues with generics & inheritance probably.
The bottom line is that without extra annotation from the user, flatten with an arbitrary level to flat can't be typed.
I would avoid those idioms (as much as possible) in the stdlib. So for now I think is better in a separate shard.
@vladfaust for the flatten(1) you can do, as pointed in the PR mentioned by @Sija,
[1, [2, [3, 4]]].flat_map &.itself # => [1, 2, [3, 4]]
@bcardiff Well, you could implement flatten(n) by doing the flatten 1 you pointed out above using a recursive macro that recurses through n, which also would get the types right. Limited by macro recursion limits, but still.
Though the question is if it is worth it when flat_map &.itself is available - it fills a lot of the actual need for the feature.
Yes, a macro could do the trick if done well, yet it will be harder to explain to the user why it is Array.flatten(arr, level).
I haven't need flatten with other than 1 unless it is a random exercise.
Confirmed, I never needed flatten other than 1 or 0 levels. I've written down flat_map &.itself thing, should fit my needs 馃憤
flat_map &.itself is a bit of a weird trick, it would be nice to get a flatten_once method
@RX14 @bcardiff I finally created a shard based on the code I presented previously. I had to modify the code quite a bit to make it work in a number of test cases. However, now I have an implementation that passes a good number of tests.
The shard is here: https://github.com/jbomanson/flatten_as.cr
I settled on the following kind of API:
[1, [2, [3, 4]]].flatten_as(Array(Int32 | Array(Int32))) # => [1, 2, [3, 4]]
As a side effect, this can do deep conversions between Enumerable types while flattening. For example:
[[[[1]]]].flatten_as(Array(Set(Int32))) # => [Set{1}]
As a final caveat, there are cases involving union types where it is not completely clear what the output should be. Consider this example:
[1, [[[2]]]].flatten_as(Array(Int32 | Set(Int32))) # => [1, 2]
Here, it is conceivable that the user wanted the output to be [1, Set{2}]. The reason for the actual output [1, 2] is not completely straight forward, but the general description in my shard hopefully helps understand it.
Most helpful comment
@jbomanson that's an interesting idea, I'd love to see a PR! Just on the API side, I think
flatten(to: Array(Array(Int32) | Int32))would be a more descriptive way of expressing this, if a bit longer.