« BackFun with Go Iteratorsxnacly.meSubmitted by xnacly 3 days ago
  • integrii 40 minutes ago

    Call me crazy, but I don't like any of this. Make more named functions. Keep your logic flat and explicit. I believe go wants you to code this way as well. Imagine the horrors this kind of function chaining creates. Actually, you don't have to. It's JavaScript.

    • lifthrasiir 32 minutes ago

      Even JS doesn't really use functional styles that much. In fact, the whole reason for functional styles is the decomposability: any language with easy function chaining like `x |> f |> g |> h` will also have an easy extraction of any part of the chain (say, `x |> fg |> h where fg = f . g`). It is not a good idea to use chaining when the language supports no such feature, as it would be much harder to work with such chains then.

    • gtramont 2 hours ago

      Unfortunately the chain approach breaks down when if you need to `map` to a different type, for example. Go does not allow generic typed methods.

      • simiones 23 minutes ago

        You can actually (almost) make it work, you just need to add an extra type parameter to keep Go's silly limitations happy [0]:

          strs := []string{"abc", "defgh", "klmnopqrst"}
          ints := From[string, int](strs).
                   Map(func(a string) int { return len(a) }).
                   Filter(func(a int) bool { return a >= 4 })
          Iterator[int, float32](ints).
                   Map(func(a int) float32 { return float32(a) }).
                   Each(func(a float32) { fmt.Printf("%v\n", a) })
          //prints 5, then 10
        
        If they allowed the postfix cast syntax to work for non-interface types too, it could have been a single chain, actually (you could do `.(Iterator[int, float32])` inline instead of needing the extra variable.

        Note that the original implementation in the article modifies the collections in place, in which case this issue doesn't come up at all: you can't put strings in an array of ints. My implementation creates copies of these collections so that the concept of mapping to a new type actually makes sense.

        [0] https://go.dev/play/p/ggWrokAk7nS

        • daghamm an hour ago

          I never understood why that limitation exist.

          Can someone explain the reason for this?

      • skybrian an hour ago

        I think it would be more idiomatic to use statements, not expressions. That is, it’s ok to use local variables for intermediate values in a pipeline.

        • simiones 22 minutes ago

          You often end up with a lot of extraneous variables with no useful names if you do that. A lot of the time the intermediate results in a pipeline are almost meaningless.

          • skybrian 3 minutes ago

            The main thing is not to do things that would be fine in other languages when they result in complications in the one you’re using. Some people want to write an entire library rather than writing statements. Why?

            Also, local names can sometimes be useful documentation when the function names don’t really get the point across (perhaps because they’re at a different level of abstraction). Or alternatively, in Go it’s idiomatic to keep them short.

        • pdimitar 2 hours ago

          Might be interesting to make a library that competes with https://github.com/samber/lo?

          • mseepgood 25 minutes ago

            It's not interesting. Boring people did this a million times, the result is always the same: just use for loops.

            • gtramont 2 hours ago

              Back when the Go team announced generics, I had a go (pun intended) at it: https://github.com/gtramontina/go-extlib -- might resurrect it one day. `lo` is pretty comprehensive though.

              • mihaitodor an hour ago

                Here's one: https://github.com/szmcdull/glinq It doesn't do function chaining though.

              • dilap an hour ago

                The Reverse implementation seems off to me -- it runs through the iterator twice, once collecting into a slice, and then a second time filling the same slice in reverse. (So basically the first Collect call is only being used to find the length of the iterated sequence.) I'm not sure about Go conventions†, but I imagine it would be considered better form to only run through the iterator once, reversing the collected slice in-place via a series of swaps.

                († Are iterators even expected/required to be reusable? If they are reusable, are they expected to be stable?)

                • jerf an hour ago

                  That eliminates one of the main reasons to use this approach. Function chaining as most people write is awful for performance because it involves creating a separate array for each step in the chain. Given that most programs are actually more memory-blocked than CPU blocked, this is a bad tradeoff. Composing Go iterators, and composing iterators in general, is preferable because it doesn't have to create all the intermediate arrays. A bad reverse wrecks that back up.

                  Still, you need the option, and while reverse is one of the more common iterators, it's still usually avoidable if you need to. But if at all possible I'd suggest a "reverse" type-specialized to slices, and as necessary and possible, type-specialized to whatever other types you are using to actually crawl a value "backwards" rather than collecting a full iterator into a slice.

                  (Then again, I'm not a fan of this approach in imperative languages in general, due to the generalized difficulty in refactoring code written in this style and the fact that observationally, people just don't refactor it once written and it affords a style rife with repetition. One of the most important considerations about code is how easily it can be refactored. In Haskell, this approach is excellent, precisely because it refactors very, very well in Haskell. In imperative languages, it tends not to, thus, serving as another example of why you can't just blindly bring over nice things from one language into another without verifying they haven't become sour in the process.)

                  • dilap an hour ago

                    Yeah, that all makes sense!, but I think it's not relevent to the code in question, which is this:

                        func (i *Iterator[V]) Reverse() *Iterator[V] {
                         collect := i.Collect()
                         counter := len(collect) - 1
                         for e := range i.iter {
                          collect[counter] = e
                          counter--
                         }
                         return From(collect)
                        }
                    
                    So this code creates a slice from the iterator in the call to Collect(), and then fills the slice again in reverse by running the iterator again, which I think is wrong (or at least not-ideal).

                    (Your broader point about wanting to avoid creating an intermediate array at all for iterators and using type information to intelligently reverse "at the source" definitely still stands, in a broader context, though.)

                    • jerf 38 minutes ago

                      Oh, yes, sorry. The level of error was more than I expected.

                  • atombender 44 minutes ago

                    Why reverse the slice at all? Collect the input into a slice, then return an iterator that navigates the slice in backward order.

                    What this sort of thing lacks is any ability to optimize. For example, let's say the operation is Reverse().Take(20). There's no reason for the reverser to keep more than 20 elements in its buffer. But to express that, you have to make sure the iterator can be introspected and then rewritten to merge the operators and maybe unroll some of the loops to get better cache locality. This is what Haskell can achieve via Stream Fusion, which is pretty neat. But not so viable in Go.

                    • dilap 27 minutes ago

                      Thanks, good points.

                  • Savageman 2 hours ago

                    I like how the author uses a test to run arbitrary code, this is exactly how I do it too!