4

I’ve abandoned the classic for loop from my tool belt for quite a long time now. The vast majority of the code is functional.

But today I’ve encountered a problem where I’m considering to use the imperative for loop again because I can’t come up with a good functional approach.

Maybe you guys have an idea.

I have a list of items and I want to make a new list which is like the original list, but it has extra items in between of some other items.

The tricky part is that there is a condition that needs to be checked for each pair of items to determine if the new item should be inserted in between. Otherwise nothing should be inserted.

Comments
  • 1
    Alternative description:

    There are two kinds of items in the original list: Kind A and kind B.

    Between each consecutive item of kind B, insert a new item of kind C.
  • 1
    Map, then return either just the original element itself, or that plus the new element, then spread. Order might be tricky though.
  • 1
    @kamen With map, I don’t know if the second item is also of kind B.
  • 1
    @kamen but you gave me an idea:
    I could make a zip with the original array and itself again, but without the first element so that I get a list of adjacent pairs.
    Then I map those pairs to either the first element of the pair, or to first+C, if both are B. Then flatMap (spread?) into a list again.
    🤔 that should work. Thanks.
  • 1
  • 2
    Sounds like you want a reduce/Aggregate.
  • 2
    @Demolishun nope. Swift. But the functional principles are all the same.
  • 1
    @spongessuck I don’t see how reduce would help here
  • 1
    @Lensflare Has Swift Generators?

    This sounds like a very good use case for it...
  • 0
    @Lensflare pass in a mutable list as your initial value, then you check your last item in the list and current value to see if you need to insert the additional item, then add the current one, then return the list.
  • 0
    Or you could avoid a mutable list by starting with an array or whatever and just concat'ing as you go.
  • 0
    @Lensflare You didn't say which language. Depending on the implementation you might have the index and the array available in the callback. I was thinking JavaScript since that's what I'm dealing with most often, but other languages have this too.
  • 0
    @IntrusionCM you mean like yield in C#? No.

    But I don’t see how generators can be useful for this. Unless for optimization, but I don’t care because the list is very small.
  • 0
    @spongessuck I don’t get it ^^
  • 0
    @kamen I didn’t say the language to not discourage people which are not familiar with it. And I am more interested in the idea rather than an implementation. Because it’s possible in any language.
    If some function doesn’t exist in a language, it’s easy to build it with functional composition and basic building blocks like map and reduce.
  • 0
    @Lensflare

    If I understood you correctly:

    List: 1 ... N items

    Chunk list in pairs, so 2 items per chunk of the list.

    Then you pass each chunk with a pair to the generator - checking your condition.

    The generator either returns the original entries or the modified / appended entries.

    Add the return of the generator to a new list unpacked.

    Or did I misunderstood you?
  • 0
    I will try the following tomorrow:

    Zip into itself to get a list of adjacent pairs.
    Map pairs to lists of either the first value from the pair if condition is not met, or the first value from the pair plus the new in between value, if the condition is met.
    Add the missing last element to the list.
    FlatMap the resulting list of lists into a single list.

    With appropriate naming, this should look quite readable in code.
    If it doesn’t, I will try the imperative approach and see if it is more readable. :)

    This will also be a great use case for a unit test.
  • 0
    @IntrusionCM ah this sounds like my idea, but without generators.
    Now I see how generators would help, but it’s ok, I can use flatMap.
  • 0
    const result = original.reduce((ar, curr) => {
    const intermediateValue = getIntermediateValueIfNeeded(ar.at(-1), curr);
    if (intermediateValue)
    return ar.concat(intermediateValue, curr);

    return ar.concat(curr);
    }, []);
  • 1
    @Lensflare Generators are idiomatic equally to a map with a lambda.

    In practice, they work differently of course. But idiomatic it's the same.

    I just googled if swift has sth like a chunk function...

    Seems like you could use stride with by: 2, followed by a map... The map could check the condition and return the elements or your modification.

    Seems functional to me...
  • 0
    @spongessuck
    what’s getIntermediateValueIfNeeded()?
    And what is array.at(-1)? Some JS specific magic?
  • 1
    The function is whatever you need to run to determine if you need to put the intermediate value in/get the intermediate value you need to put in.

    Yes Array.at(-1) gets the last item in the array.
  • 0
    @IntrusionCM that’s what I intent with the zip function, but I’ll look into strides, too. Thanks.
  • 0
    @spongessuck ah, I see. Thanks.
  • 2
    Replace all instances of 'Kind B' with 'Kind B followed by Kind C'.
    Then replace all instances of 'Kind C followed by Kind A' with 'Kind A'.
    Finally, if the last item in the list is of 'Kind C', remove it.
  • 2
    Why the avoidance of the for loop and mutating an array? don't get me wrong I love array methods .map.filter.flatMap.reduce etc.. but you gonna da 3-4 iterations when it can be done in 1?

    const copy = Array.from(all)
    for (let i = copy.length - 1; i--;) {
    if (someCheck(copy[i])) {
    copy.splice(i +1, 0, someNewItem)
    }
    }
    return copy

    (Nb: untested might have overlooked sth)
  • 1
    @webketje because that's not the functional way to do it.
  • 2
    sounds like you already know the best tool for (heh) task, and your only issue is that you don't know how to do the task by your preferred method, because that preferred method isn't really suitable for that task?
  • 3
    this discussion is fascinating.
    so much time and brainpower invested into expressing a straightforward operation in such an obnoxiously opaque way that when you finally figure it out you can just send it to thedailywtf.com yourself instead of waiting until your successor finds it and sends it.
  • 0
    One question, do functions like map guarantee to be in the order of the array?

    I know when I have used these functions in C++ the order seems to be the same as the array. But I was not sure this was by convention or by spec.

    Just checked C++ reduce:

    "in other words, reduce behaves like std::accumulate except the elements of the range may be grouped and rearranged in arbitrary order "
  • 2
    @Lensflare all of that just to not let the computer do its imperative job as usual
  • 3
    Not wanting to use for-loops but map() is fine? O.o"" (That's a for-loop under the hood, and usually writing that for-loop by yourself runs faster than doing it by map() ).
  • 1
    @Demolishun yes, map preserves order.
  • 0
    @lankku @iiii @Midnight-shcode runs faster doesn’t matter for a list of 10 items or so.
    The whole idea was to find a readable functional solution, if there is any.
    I’m not afraid of the classic for loop and will use it if turns out to be more clear and readable.
  • 0
    @spongegeoff that’s a clever idea!
  • 1
    @Lensflare it is also MUCH more readable than the hodgepodge of zip-map and other stuff just to avoid a simple iterator.
  • 0
    My solution now:

    sections.adjacentPairs().map { pair in
    if pair.0.isA() || pair.1.isA() {
    return [pair.0]
    } else {
    return [pair.0, itemC()]
    }
    }.flatMap{ $0 } + [sections.last].compacted()

    adjacentPairs() is available in the official Algorithms module.

    I find it more readable than the imperative solution. That’s of course a matter of taste but I’m happy with it.
    Thanks for the ideas!
  • 1
    @webketje Your solution adds C after ever instance of B, but i think the OP only wants to add C between successive instances of B?
  • 3
    @spongessuck "because that's not the functional way".. err so? I always prefer the *practical* way.

    @spongegeoff my solution doesn't suppose anything. someCheck could return true for anything
  • 2
    @lankku i mean...
    all this "functional" bullshit is just for loops under the hood, but you shouldn't say that, it breaks their heart and ruins their illusion that their code is somehow "better" or "cleaner"
  • 1
    @Lensflare "the whole idea was to find a readable functional solution"

    i understand that. but you seem to not understand that if it takes 10+ people 2+ hours to come up with something, it obviously won't be very readable.
  • 1
    @Lensflare lol
    yeah, so much more readable than:

    // List original
    List nl = new List();

    foreach(item in original) {
    nl.Add(item);
    if(item == something) nl.Add(inbetweenItem);
    }

    have you been growing up on JavaScript or something?
  • 0
    @Midnight-shcode your code is missing the check for the next item as well. It’s not much but enough to make it similar in readability.

    And I don’t agree with your claim that if something takes a long time to come up with, means that is not readable.

    No, I hate JS.
  • 0
    @Midnight-shcode Yes, I do think that functional code is cleaner/better.
    Because it describes the intent directly. You say what your result should be, more or less declaratively. Opposed to specifying the steps needed to arrive at the result, which need to be followed mentally to understand.

    Of course this is subjective and a matter of practice and familiarity.
  • 0
    @Lensflare add
    var prevItem = null; //before the loop

    prevItem = item; // at the end of the loop

    modify the check inside the loop. done.
  • 0
    @Midnight-shcode I know how to solve this. The point is, omitting it, you made it look easier to read. It’s just an unfair comparison.

    With that extra stuff it becomes harder to understand, and therefore less readable.
  • 2
    @Lensflare hm.
    when you say "result should be a flat map of a map of pairs where xyz returns this kind of pair and uvw returns different kind of pair"

    ... is it really more clear and simpler to understand than "put an item behind/between each pair that xyz"?

    i don't think so.

    just this whole thread is about that, for fuck's sake. the whole thread is "how to translate this simple and straightforward imperative description into ANY kind of declarative one"
  • 0
    @Midnight-shcode your description of "put an item behind each pair…" is already your interpretation. It is not what the code reads like for everyone. Again, it’s subjective. For me, the functional code reads directly like this.
  • 1
    @Lensflare ok, sorry, code reads:

    loop through each item, if previous and current conform to condition, insert another item.

    one step removed from the intention.

    as opposed to whatever you did which says, if i even understand correctly, something like "map this list into pairs, fork based on pairs and in one case modify the pair, then map that map into a flat map"

    how is that even declarative? map is an operation, imperative. all this "declarative" shit isn't even declarative at all, it's just imperative using different words. "map" is a command for action, not a declaration of result.

    in short, this is a fad.

    don't get me wrong, I like LINQ, it's nice in many cases when it makes operation less wordy, and via that, easier to read.

    not in this case.

    i find that this pseudo-declarative style is best for a sequence of linear (non-branched) operations where each one is relatively simple, and you chain multiple of them.

    which is not this case. the core operation forks.
  • 1
    @Lensflare

    why do you think people hate branches in SQL statements, or stored procedures (because they contain branches) in general?

    for the same reason why trying to do something like this "declaratively" (there's nothing declarative about it, let me repeat that) is stupid.

    if it weren't stupid, branching SQL / complex stored procedures wouldn't be awful either.
  • 0
    It is declarative in the sense that you declare the rule how each element maps to another element to get a new list.
    How map arrives at the result and what or how may steps it takes to do it, is hidden details.

    For me, it reads like this:

    Make pairs of adjacent elements, which are mapped to the first element, or the first plus the new intermediate element, depending on the condition.

    The meaning of flatMap to collapse it into a normal list again is implied. That’s what flatMap is for. (Again needs familiarity)

    I admit that the thing with the first element or the first plus intermediate is not directly telling the intent.
    That is the part where the imperative solution is more direct.
    But then again it doesn’t require an index or an extra variable to track the next or previous element, so it wins on this point.
  • 0
    @Midnight-shcode I’m not familiar enough with procedures in SQL, so I don’t get your point about branching 😄

    But let me tell you that I hate SQL even though it is declarative.

    I hate it because it is designed for relational data.

    Functional programming styles are not limited in the same way.
  • 1
    Damn, this rant is completely out of control! 😅
  • 2
    @Midnight-shcode "all this "functional" bullshit is just for loops under the hood"

    That's exactly the point. Don't write the for loop yourself. Tell the compiler/interpreter how you want to transform your collection to get a new collection.

    The imperative way is just clearer because you're used to it. Map and flat map are perfectly clear to me, and you don't need a dumb out-of-scope mutable variable and List to make it work.
  • 1
    @Lensflare "functional" way of dealing with lists when it involves more than the current item does not ever clearly show any reasonable intent.
  • 1
    @Lensflare If I saw that code in a codebase I would probably be confused about all the indirection with the intermediate array creations. Like it literally creates 4-some new arrays + 1 or 2 times all items. Do you find immutability suitable in most if not all cases? (I'd say I'm at 50/50)

    You said "that is the part where the imperative solution is more direct", but that is the only part that really matters: it's the business logic that drives the contents of the result.

    I try to write code like how I was taught to tackle early (end-of-primary / start-of high school) math problems: list your variables at the top, do your operations, and return the result.

    This can be read lineary in steps, which is what most of us are familiar with and it provides a clear separation between the definition phase, the operation and the final phase of a func.
  • 1
    @iiii I disagree with that. Having a decoupled iterating func(item, index, arr) can be useful in case it needs to be reused in different places or ways (eg it could be used as a standalone func call instead of an array method)

    PS The only "functional-minded" devs I really have a problem with are those who use map instead of foreach
  • 2
    @webketje you should only use foreach instead of map if you're doing I/O on each item in the array, e.g. saving to db, sending http requests, etc. If you're getting some value out of your array, you should use map, reduce, filter, etc.
  • 3
    Is this functional or fucktional programming? lol

    I have found this an interesting read.
  • 2
    @webketje sure thing. Depends on a use case
  • 1
    @webketje I find immutability useful in most cases, yes. Not all obviously, but most.
    I especially dislike everything that needs extra variables to track some state by iteratively assigning some value and checking it in the loop.

    I’ve said this many times already:
    This functional style is not always better. It depends on the use case and the familiarity of the devs that have to deal with the code and of course also preference.
    But: For me, the functional style is better in *most* cases.
    And I say this as a dev who have spent the vast majority of years writing and reading imperative code.

    One more thing, even if it doesn’t apply to the example here:
    Some devs equate simplicity and readability with the style that they are used to. I’ve encountered it many times in my job.
    It’s very hard and annoying to argue with that, as you might see why.
    It’s subjective.

    We can only argue about safety aspects somewhat objectively.
    And one of the safety aspects is immutability.
Add Comment