The thing is that Go iterators are primarily there to get people to not use channels for iteration in cases where you just needed an iterator abstraction. So yield just works exactly like "func that pushes into channel" but an order of magnitude faster (yield just calls the loop body instead of going via the goroutine scheduler) and you can refer to shared variables without race conditions. If you want a pull based iterator you call iter.Pull on the rangeable func and much like the Go keyword it will create a thread-safe coroutine like in Lua. The approach that Odins creator mentioned here works only for iterating over data structures, but not for usecases like iterating over a database cursor with automatic cleanup at the end, or something nontrivial that actually behaves like a separate process, much like Python generators. The pro and the con of go iterators is that they are goroutines without the overhead of synchronizing across threads. Also, the func type signatures will be cleaned up into an alias (1.23 allows parameters in aliases) so that the first line would be func Backward[T Any](s []T) Seq[T] { ... }
Ding ding ding. This is the correct answer. The Odin-style iterator works fine for finite data structures (where an "i" makes sense), works less well for a continuous stream (like gRPC streams), poorly for lazy streams/paginators/cursors, and falls on its face where you want to interact with the inner state in any way. In python it's the difference between Collection (has len, iter, and next methods), Iterable (has iter and next, but no len), Iterator (has next), and Generator (has next and yield from). The merged proposal can do all of those, the struct iterator can't do it without significant complexity. I find the syntax a bit weird but it's tolerable once you spend some time to wrap your head around it.
Normally I feel bad about listening to video essays in the background because I feel like I'm missing out on important context. Videos like these alleviate that for me because even if I do watch it I don't understand any of the code onscreen
All of the new changes are all for Kubernetes. Every knew K8S version depends on it to the point that they had to delay the deploy of I think it was 1.24 or 1.26 because Go dropped a new version the week before they were going to launch. I listen to the Kubernetes pod cast by Google.
A go iterator method built off of interfaces and return types would have been clearer. So something like this: for i, v := iterate (int, string) foo{} Where foo meets the interface interface{ Init() Next() (int, string) } Or something like that.
@@KevinLyda No. It’s just that Go in many ways avoids the need to write state machines through its light weight goroutines. A worker that reads from a channel, computes a new value and writes it to another channel can be written using a single function without the need for custom structs. This iterator design simulates how you would write it using channels although it doesn’t involve concurrency.
@@krumbergify nitpick: it involves concurrency (interleaving code counts), but doesn't involve parallelism (goroutines/threads) > Concurrency is when two or more tasks can start, run, and complete in overlapping time periods. It doesn't necessarily mean they'll ever both be running at the same instant. For example, multitasking on a single-core machine.
This makes me appreciate Lua iterators a lot more. Given some similarities between Go and Lua, I think Go would’ve been well-served by the Lua style iterators. Although there are things that are dealbreakers as in eyes of Russ: state is not stored in control flow, defer doesn’t really work for cleanup, Lua doesn’t cleanup after iteration ends (which might be fixed by the gingerbill’s onBreak boolean flag)
For anyone still confused, here's an actual explanation of what's going on and where the yield function comes from. Go takes your for loop's body and desugars it to a function that takes the loop variables as arguments and processes them. This is what Prime meant how it's similar to a forEach in Javascript. If your loop body terminates it returns true so the outer function re-excecutes yield, however a break in the loop body desugars to 'return false' in the function. Similarly continue desugars to an early return of true in the function. All that said I strongly feel this should've just been a Ranger interface where these mapping are less obtuse, but range is inherently magical so maybe that's why they didn't care so much.
I thought I was about to get roasted when I saw my sequence diagram in the thumbnail. I’m still not sure how I feel about the iterator spec. There are definitely readability issues, and a type called Seq2 seems more like something from a third-party library. I’m cautiously optimistic though.
This happens in every language which gets wider adoption. Authors somehow lose track what got people attracted in the first place. They suddenly try to accommodate people, which were just force to use the language and do not like it at all. This will be used to actually get teams off of Go instead of onboard as the "haters" will just point to this and argue that if you cannot understand it right away, Go is not really as simple as advertised and "we should go with Rust".
@@hamm8934 a really good question. I know people (who do not hate programming in Go) were waiting for arenas with excitement as it was able to address very real and specific class of performance problems. I know zero people, which were like "I like how Go was doing things so far, but I really want some high-order-functional construct directly in the language, which I will need to relearn every time I want to implement that."
I saw once very through presentation on some C++ conference when guy analysed through all kinds of iterators in many languages with pros and cons, very interesting
Article author do not understand completely C++ iterators, they do not have `iterator_next()` as they need work for subranges too. This mean you can find two iterators in any range and use them in any place where you could use whole range iterators. Another benefits is all algorithms that correctly work with iterators will work with pointers that make range too.
I reckon Bill probably does understand C++ iterators. Having watched and read a few of his blogs and interviews, he’s predisposed towards simplicity and straightforward semantics. So copying C++ anything is likely a non-starter.
Return sends a specified value back to its caller whereas Yield can produce a sequence of values. We should use yield when we want to iterate over a sequence, but don’t want to store the entire sequence in memory. Yield is used in Python generators. A generator function is defined just like a normal function, but whenever it needs to generate a value, it does so with the yield keyword rather than return. If the body of a def contains yield, the function automatically becomes a generator function.
Also, the main difference from a *practical/performance standpoint* for Python is that when your iterable uses a generator function to instantiate instead of an iterator object, instead of being compiled by loading the entire instance into memory, __next__() calls the function, so it only needs to keep a single value in memory at time.
That design of iterator also makes it _trivially_ easy to disregard break and possibly leaks the inner closure too. With iterator being struct there's no way for the iterator to know the flow of inner loop at all.
@@metaltyphoonC#'s iterators follow an almost identical structure to Rust and Odin, not like Go. The yield keyword is just syntactic sugar over top of it which generates the closure and state machine. You can do the same thing in Rust by using generators.
@@metaltyphoon I mean, it's more of generators and *not* iterators right? It's not just passing values, but actually influence the inner loop, possibly calling it from *outside* the loop or even other goroutine (which is another huge can of worms).
It's actually sad how many problems are created by the lack of support for genuine generics. Yes, it's slow to compile, but there's a reason most high level strongly typed programming languages support generics. They're super useful for lots of things, and certain problems can't be easily solved without generics. I'm not sure if it's funny or sad how much Go refused to support proper generics.
It reminds me of how D does custom iteration. First of all, there are both approaches, i.e. the push and pull approaches. A type that implements the pull approach is called a range. A range has the following members: empty (returns true if it’s done), front (gives you the current element), and popFront (steps forward). A "foreach (x; xs) { … }" is equivalent to "for (auto copy = xs; !copy.empty; copy.popFront) { auto x = copy.front; … }". Most algorithms, such as iota, map, filter, reduce, etc., are based on ranges. However, sometimes iteration needs nontrivial state, such as walking a tree. For that, the push approach is better. In D, you give your type a member function named "opApply", which takes a callback delegate as a parameter. The body of a "foreach" is transformed to an appropriate callback for you. When "opApply" calls the callback, the callback returns 0 indicating "go ahead", or something nonzero, which "opApply" is supposed to return immediately. The transformed foreach body returns non-zero on break, return, goto, etc. (not continue, though). The neat thing is, the D compiler as a switch that lets you see the code after such lowerings.
The opApply can be overloaded with different-arity callbacks so that you can use "foreach (x; xs)", but also "foreach (i, x; xs)" or whatever number of arguments the callback takes. I have once written a tree type that has opApply walk the tree and provide an optional index, which is an array of indices that tells you which branch on the respective node was taken. Because "opApply" can re-use the index array, it can be allocated on the stack if the tree height is known in advance.
I think the reason for this is that for the easy models, Go already has and does what it is supposed to do. The design is literally meant to have full flexibility to implement support for everything else in Go. For anything the simple models support, you can already just use a loop with a channel. Works exactly the way you want and is cheaper. The whole idea of the proposal is to support everything that this approach cannot support.
rust is more readable than go in my opinion. it's kind of a combination of javascript and ocaml syntax which i find to be readable. in theory go should be more readable but it requires a lot of boilerplate and i find boilerplate in go to be ugly and hard to read.
@@matress-4-2323 as a beginner in Go, I have no problem understanding any codebase, idk what unreadable boilerplate you're talking about. Go is by far the most readable lang I've used, but then again I've never touched rust so far. But what I hear from others about Rust is everything but a nice readability experience
The function returning a function isn't a bad approach, but instead of the whole iteration and the yield function stuff, the iterator generator should return a next function like: func backwards[T](slice []T) func() (int, T, bool) { // setup everything return func() (int, T, bool) { // backwards access } }
I do agree making go iterators use an interface does make sense. fun fact we can sort any arbitrary datastructure in go by fulfilling an interface, so iterators would have fit right into this model.
In my mind, whenever Prime says "I wish they would have done it the *Rust* way". I can almost always just replace Rust with Java and it still works. Because he's usually just talking about interfaces anyway xD.
If you can replace it with Java, then you can replace it with C#. If you can replace it with C#, then you can replace it with BeefLang, - the ultimate language of all programming 🐮
This Go proposal still allows for vectorization optimizations. I wonder if they could do inlining as well (maybe when doing PGO) of the for loop's code block, which is probably non-trivial as the block is passed around as a function pointer with nested layers of closures.
I think the explanation of push vs pull was rather confusing, if not confused. Mentioning that JS array methods operate on the whole array before the next method happens is talking about eager vs lazy evaluation, both push and pull iterators can do both - though what Prime is probably getting at here is that it's a lot easier to do eager with a push iterator than lazy, while both are easy with pull. The thing named iterator (or enumerator) from you're probably familiar with is a pull iterator, it has some "next" method you call to get either the next value or that it's done. A push interface is the opposite: you pass the iterator a "next" method that receives the items in the control of the iterator. The trade-off is generally that pull iterators are much easier to compose, but depending on the language much harder to write (without some sort of generator syntax at least) as they need to keep internal state between calls. Push is, in a way, really common in many situations you don't think of as being iterators at all, namely event listeners.
Basic text files are also without a standard for encoding, but a smart user will use UTF-8 and modern software will usually default to it. But before Unicode created a standard, it was very hard to decode text files. Unfortunately, they first created standards that would make every character 2 or more bytes. Because of this, many applications have to have an algorithm to try and determine the encoding. So, being surprised that CSV doesn't have an official standard is mostly due to the encoding issue of the file that the data is stored in.
It's worse than that though. CSV makes no claims about when to use quotes or not, LF vs CR vs CRLF, escape characters, illegal sequences, unmatched quoting. Parsers and emitters are basically whatever "feels right"
Every time I hear Prime describe what a language should be I think he's describing C#. You can't make a language so simple that people need to write everything every time, you need to make a language simple enough so everything you need to write should already be written and provided in the standard library. That's C# and .NET!
I agree that the syntax is not great. However, I disagree with the general statement that pull-based iterators are strictly superior than push-based. The beauty of a push-based iterator is that any existing data structure traversal code can be converted to the push-based version. This is almost as simple as replacing your print function with the yield function in relevant places. This is not so easy with the push-based, especially if you are using recursion in your implementation. Converting simple recursive algorithm into a pull-based iterator is not that simple. Another factor is a performance. In a pull-based version you do end up with more branches since the implementation has to restore its current state on every iteration. This is probably less of an issue for simple traversal algorithms. Another problem with the pull-based approach in C++ is that your loop state is managed in different places which makes it harder to reason about loop invariants. Where pull-based iterators shine, indeed, is composability. You can easily implement various constructs on top of it, such as iterating two data structures in parallel. This would be impossible in a push-based version without buffering all elements from one of the sides. So, back to the Go language proposal. Maybe it is in the spirit of Go’s simplicity.
Very similar to Python iterators and other languages. Go seems to have made an overly complicated mess of a problem that has been clearly solved already.
@@101Mant Yeah, things like the simplicity of having an iterator that is not an iterable, simply by using a generator function/using a yield statement so that you don't have to load iterables that are unfeasibly large, while having to change *nothing* else, is a perfect example of why I just zone out all the hate Python gets and finish POC projects 10x faster than other teams. Haha.
That's generators, strictly: a way to implement the iterator interface. (Actually IEnumerable/IEnumarator) Iterators are more about having an easy way to consume sequences (for each) than to define them, but really you want both.
@@SimonBuchanNz At least in Python's parlance, an iterator is a built-in class that takes either: * an iterable (an object, like a list, tuple, etc) which is iterated over, calling __next__() after each iteration to retrieve the subsequent item in it, or * a generator (a function that ends with a yield statement, and each time __next__() is called the function is called again to generate the next term in the sequence instead of loading a whole iterable when the class is constructed)
Nim: iterator reversed[T](x:openArray[T]):T = var idx = x.high while idx >= x.low: yield x[idx] dec idx var nrs = [1,2,3,4,5,6,7,8,9,10] for nr in reversed nrs: echo nr
I don’t have time to watch the full video so I might be missing context, but it seems really strange to me that channel syntax and go routine syntax wasn’t used. Really there’s not much going on here aside from having a go routine and channel that can swap back and forth without hitting a scheduler / mutex. Could have the syntax be like, out := go func(){ out
One of the main points was performance, because folks were using channels for iterator-like behavior. Channels have a mutex and involve the scheduler. Iterators are pure function calls and have order of magnitude less overhead.
Honestly the only thing that's missing for me is an easier way to deal with common errors. Zig does this excellently by implementing error returns as a seperate language construct rather than using multiple returns, which allowed them to easily implement operators to early return errors
Same for me. It butthurted me so much right after announcement. We were ok without it and now instead of well known N approaches of doing things we'll have N+1 approach. Already imagining zoomers and boomers arguing in the comments under every PR about doing/not doing sht though iterators
Dude also has no idea why C++ iterators work the way they do. Anyone who wants to _actually_ understand this stuff and the tradeoffs being made should watch the talk "Iterators and Ranges: Comparing C++ to D to Rust" by Barry Revzin.
@@isodoubIet exactly, every time they need to compare how c++ handles this, they show how the expression is defined behind the compiler and not how it's actually used, it says a lot about the person... "hey, let's compare it to c++" - put the "real" c++ code in cppinsights and show the code to the compiler's eyes
@@justanothercomment416 And if you do end up having to write your own, it will automatically work correctly with all the algorithms in the standard library, some of which are flat out impossible to write in simpler iterator models. Alex Stepanov's STL is a work of genius; anyone trying to criticize it should at the very least understand it.
the iterator syntax, while it has sense to it, looks like pure chaos. the whole function that returns a function that returns a function makes it look quite awkward.
Watching programmers argue, complain and make memes over complex unreadable syntax and calling human readable syntax "lame" is bizarre. I don't ever want to be a "programmer", I just want to learn how to create solutions for things I personally need done. I'd rather be on the Evan Czaplicki or Richard Feldman side of things. Thanks for reminding me why, genius system/low-level programmers. 🤣✌
Both generators and first class async are almost the same thing: a persistent scope/continuation. If they made this thing a single concept from the language side, that would be one thing... Generators are great, can be a lot cleaner than struct (because the struct IS the scope), but the way they designed it for Go doesn't make sense to me.
compared to other changes in GO (everything isn't internal), iterator involve very little ammount of go contributors and i think their decision are biased. "if you can't decide, the answer is no." they shoudn't standarize iterator. but instead provide a best practive guides.
The author admits himself to be imperative programming oriented. I think this is why the imperative variant looks easier to him. To me, the variant with yield is more straightforwards, maybe because I went through a functional programming (learning) phase. With functional programming concepts being built-into languages these days, I think new coders might agree
I don’t know how you can call Odin’s iterators simpler. I’ve always absolutely hated implementing pull iterators. I feel like Go’s approach is an order of magnitude simpler and less error prone.
@marcsfeh When composing with other iterators, it’s much harder (maybe even impossible) for the cleanup code not to run. I have definitely messed that up in pull based iterators before.
Hey, Prime, random question: Why i cant watch 4k content on netlfix even if i have the 4k subscription and the HDCP protocol. Is there some sort of limitation because i want to watch 4k netflix content on my 2k screen but i cant so i have to make a custom res of 4k and then check netlfix to see that there is really 4k quality, which it is. Why just cant display 4k content on 2k display just like yt?
I'd have more sympathy for Rob Pike's statement about Googlers not being able to appreciate a brilliant language if I knew of a brilliant language he himself had designed earlier. As it is, it feel like several of Go's design deficiencies have more to do with Pike and team's unfamiliarity with non-C-family language features. For example, in ML sum types & pattern matching and type inference & generics work together brilliantly but it's still plenty simple to learn and use. I appreciate Go's simplicity, but I can't shake the notion that it would have been a much better language while only a tiny bit more complex if it had some of these features, and further that if they had been familiar ML or similar that it wouldn't have taken them a decade to figure out how to do generics.
Seems like one of those weird things where you could just "not use it" in a different sense than how C++ is. It's not so confusing that you can't understand it without using it.
Any iterator ultimately stores state, rust just makes it explicit. The downside of this in rust is that it requires lifetems which are unfun. In go it would have been so easy to do with no additional syntax.
There's a reason Go couldn't implement pull iterators with an interface. Something like type Iterable[T any] { Iter() Iterator[T] } type Iterator[T any] { Next() (T, bool) } Seems like a very nice interface. The problem, and this was called out, is what if someone defines a type like `type MyType chan int`, and then implements Iterable on MyType? Now, if you for range over an instance of MyType, it's unclear as to whether you should get an iterator and call it's Next method, or whether you should receive values from the channel. By making both push and pull iterators functions, you're making them unique types, rather than existing types that implement an interface, so this problem goes away.
Just prevent implementing the iterator interface on channels. This is the same as how in rust you cannot impl iterator on the Range struct or any other struct that is already iterable. A neat way of doing this is to make a channel internally use the iterator interface.
I despise this version of Go iterators. Far too hard to understand what's going on at a glance and very deeply nested. Go's designers aren't idiots so I'm sure there are very good reasons they're championing this design, but C#/Rust/Java got it right with an Iterator interface that you pull values out of.
I like go a lot, and honestly I think they managed to find a solution that is very much in the spirit of go's simplicity. Like so much of the language, it requires a little bit more verbosity from the programmer, but it's clear what's happening under the hood.
That its a non-standard brings problems everywhere. Some are separated with a comma, some are separated with semicolons or tabs, some have quotes surrounding the values and some have them only when the separator is included in the value, some use haphazard json-typing inside the values, some have more than one header-row and so produce multiple tables, some allow newlines inside values, some use backslash for escaping and some don't, some programs produce a different syntax depending on locale and don't know how to read all of their own produced formats (e.g. excel). Standardization would do it good.
I haven't thought about it much, but does "the odin iterator approach" require that each slice be fully baked out into contiguous memory, or can it deal with sparse generation/population? cause I think other iterator approaches might be more generalizable. is that difference the difference between "push" and "pull" iterators? I haven't heard those terms before. not sure how much that matters in practice. I have used iterators and made generators a decent amount (about a decade), and I understand the state machine under the covers, but I haven't thought comparatively about different approaches to implementing them. in places where I've actually cared about performance, I've switched to a fully imperative model that's as stupid and ugly as possible, so I could be damned sure of the memory usage and control flow patterns. but now that the subject is coming up, would be interested to see the differences.
summarizing a top-level comment here from SimonBuschanNz about push vs pull: push iterators are in control of the data, and are easier to implement (ie loop over data and yield values), but do require that ability to yield values. push iterators are like event emitters or js .forEach, where the iterator controls the data and you pass it the next function, so it can call that with each element. pull iterators can be more difficult to implement, because they usually need to keep state, but they can be easier to use on the client side. pull iterators are like calling a next function on the iterator when you want the next value, but you can choose to stop, skip by ignoring, sometimes reverse, etc. whenever you want. generators by means of go channels are push model, the iterator produces data on its terms. generators in js are pull model, since you have to explicitly ask for values by calling next on the iterator (js does allow you to "push" values by using yield. however, generators don't choose *when* the client code runs, since they don't call a function which runs the content of the loop like .forEach doesz the caller chooses when, if ever, to accept the produced yield values, by calling next) if you want to iterate over an infinite resource, like an iterator which always produces 0, you generally want a pull iterator, unless you're given a way to stop a push iterator (like your callback returning true/false) or you want it to go on forever
I will never understand why Go has such terrible syntax. It was freaking new. Everything the language is designed to do could have been done with really nice, familiar syntax, and it would have been an amazing language.
@0:42 "for key,value := range myStruct" is 'super cool feature' only problem => it doesn't exist! thought i might have missed so double checked and everything. (during which i see others saying this.)WTF FWIW here's an field iter... ''' import "reflect" func Fields(v any) (seq[struct{string;any}]){ values := reflect.ValueOf(v) types := values.Type() return func(on func(struct{string;any})bool){ for i := 0; i < values.NumField(); i++ { if !on(struct{string;any}{types.Field(i).Name,values.Field(i).Interface()}){ return } } } } '''
The thing is that Go iterators are primarily there to get people to not use channels for iteration in cases where you just needed an iterator abstraction. So yield just works exactly like "func that pushes into channel" but an order of magnitude faster (yield just calls the loop body instead of going via the goroutine scheduler) and you can refer to shared variables without race conditions.
If you want a pull based iterator you call iter.Pull on the rangeable func and much like the Go keyword it will create a thread-safe coroutine like in Lua. The approach that Odins creator mentioned here works only for iterating over data structures, but not for usecases like iterating over a database cursor with automatic cleanup at the end, or something nontrivial that actually behaves like a separate process, much like Python generators. The pro and the con of go iterators is that they are goroutines without the overhead of synchronizing across threads.
Also, the func type signatures will be cleaned up into an alias (1.23 allows parameters in aliases) so that the first line would be
func Backward[T Any](s []T) Seq[T] {
...
}
Ding ding ding. This is the correct answer. The Odin-style iterator works fine for finite data structures (where an "i" makes sense), works less well for a continuous stream (like gRPC streams), poorly for lazy streams/paginators/cursors, and falls on its face where you want to interact with the inner state in any way.
In python it's the difference between Collection (has len, iter, and next methods), Iterable (has iter and next, but no len), Iterator (has next), and Generator (has next and yield from). The merged proposal can do all of those, the struct iterator can't do it without significant complexity.
I find the syntax a bit weird but it's tolerable once you spend some time to wrap your head around it.
Thank you for eloquently putting my exact thoughts into words!
That syntax looks like a nightmare.
it does. hopefully it'll be hidden away and most of us will mostly consume these, rather than create them.
And they have the delusion it will replace C.
A nightmare to implement - not to use. That's basically the only way its justifiable. IMO.
@@sillymesilly Who? And What? Rust?
@@HypothesisIno one said it would replace C. it having a GC to begin with already tells you this lol
Normally I feel bad about listening to video essays in the background because I feel like I'm missing out on important context. Videos like these alleviate that for me because even if I do watch it I don't understand any of the code onscreen
It's okay not to look at it even if you do understand it. Golang code looks ugly AF.
@@nyx211I agree. I only look at sexy code.
@@nyx211 nah, golang have one of best looking code I have seen.
@MantasXVIIIyou seem weird. Can you try to be more normal?
@@ts-wo6pp can you try to be less boring? "EHRRM AWKWARD AMIRIGHT GUYS"
"When the language you're stann'ng for is still having discussion over iterators." shock.
All of the new changes are all for Kubernetes. Every knew K8S version depends on it to the point that they had to delay the deploy of I think it was 1.24 or 1.26 because Go dropped a new version the week before they were going to launch. I listen to the Kubernetes pod cast by Google.
Mind sharing what episode they disscuss this? im interested :)
A go iterator method built off of interfaces and return types would have been clearer.
So something like this:
for i, v := iterate (int, string) foo{}
Where foo meets the interface
interface{
Init()
Next() (int, string)
}
Or something like that.
interface IEnumerable
{
T Current;
bool MoveNext();
}
very simple
You also need a way to cleanup resources stored in the iterator.
@@krumbergify is there some difficulty with adding a third function to the interface?
@@KevinLyda No. It’s just that Go in many ways avoids the need to write state machines through its light weight goroutines.
A worker that reads from a channel, computes a new value and writes it to another channel can be written using a single function without the need for custom structs.
This iterator design simulates how you would write it using channels although it doesn’t involve concurrency.
@@krumbergify nitpick: it involves concurrency (interleaving code counts), but doesn't involve parallelism (goroutines/threads)
> Concurrency is when two or more tasks can start, run, and complete in overlapping time periods. It doesn't necessarily mean they'll ever both be running at the same instant. For example, multitasking on a single-core machine.
I thought go was supposed to be simply? That syntax was so confusing?
I had to pause the video and think about that as soon as I saw it.
Skill floor VS skill ceiling.
@@7th_CAV_Trooper With go the height wasn't very high and the house was build at sea level. Now its still at sea level but the ceiling is higher.
This makes me appreciate Lua iterators a lot more.
Given some similarities between Go and Lua, I think Go would’ve been well-served by the Lua style iterators.
Although there are things that are dealbreakers as in eyes of Russ: state is not stored in control flow, defer doesn’t really work for cleanup, Lua doesn’t cleanup after iteration ends (which might be fixed by the gingerbill’s onBreak boolean flag)
For anyone still confused, here's an actual explanation of what's going on and where the yield function comes from.
Go takes your for loop's body and desugars it to a function that takes the loop variables as arguments and processes them. This is what Prime meant how it's similar to a forEach in Javascript.
If your loop body terminates it returns true so the outer function re-excecutes yield, however a break in the loop body desugars to 'return false' in the function. Similarly continue desugars to an early return of true in the function.
All that said I strongly feel this should've just been a Ranger interface where these mapping are less obtuse, but range is inherently magical so maybe that's why they didn't care so much.
Damn... I really don't like it...
N.b.: „nota bene“ it’s a „Note:“ but with sprinkles
I thought I was about to get roasted when I saw my sequence diagram in the thumbnail.
I’m still not sure how I feel about the iterator spec. There are definitely readability issues, and a type called Seq2 seems more like something from a third-party library. I’m cautiously optimistic though.
This happens in every language which gets wider adoption. Authors somehow lose track what got people attracted in the first place. They suddenly try to accommodate people, which were just force to use the language and do not like it at all.
This will be used to actually get teams off of Go instead of onboard as the "haters" will just point to this and argue that if you cannot understand it right away, Go is not really as simple as advertised and "we should go with Rust".
How did this get through but arena allocation didnt
@@hamm8934 a really good question. I know people (who do not hate programming in Go) were waiting for arenas with excitement as it was able to address very real and specific class of performance problems. I know zero people, which were like "I like how Go was doing things so far, but I really want some high-order-functional construct directly in the language, which I will need to relearn every time I want to implement that."
I saw once very through presentation on some C++ conference when guy analysed through all kinds of iterators in many languages with pros and cons, very interesting
Link?
Article author do not understand completely C++ iterators, they do not have `iterator_next()` as they need work for subranges too. This mean you can find two iterators in any range and use them in any place where you could use whole range iterators.
Another benefits is all algorithms that correctly work with iterators will work with pointers that make range too.
I reckon Bill probably does understand C++ iterators.
Having watched and read a few of his blogs and interviews, he’s predisposed towards simplicity and straightforward semantics. So copying C++ anything is likely a non-starter.
c++ iterators are so gooood
Return sends a specified value back to its caller whereas Yield can produce a sequence of values. We should use yield when we want to iterate over a sequence, but don’t want to store the entire sequence in memory. Yield is used in Python generators. A generator function is defined just like a normal function, but whenever it needs to generate a value, it does so with the yield keyword rather than return. If the body of a def contains yield, the function automatically becomes a generator function.
Also, the main difference from a *practical/performance standpoint* for Python is that when your iterable uses a generator function to instantiate instead of an iterator object, instead of being compiled by loading the entire instance into memory, __next__() calls the function, so it only needs to keep a single value in memory at time.
That design of iterator also makes it _trivially_ easy to disregard break and possibly leaks the inner closure too. With iterator being struct there's no way for the iterator to know the flow of inner loop at all.
Non sense. In C# you can create iterations that are aware of the loop and is massively simpler. Just introduce a `yield` keyword
@@metaltyphoonC#'s iterators follow an almost identical structure to Rust and Odin, not like Go.
The yield keyword is just syntactic sugar over top of it which generates the closure and state machine. You can do the same thing in Rust by using generators.
@@Aidiakapi yes I’m aware of that. I was replying to the OP that C# iterators CAN know about the flow of the inner loop like Go can.
@@metaltyphoon I mean, it's more of generators and *not* iterators right? It's not just passing values, but actually influence the inner loop, possibly calling it from *outside* the loop or even other goroutine (which is another huge can of worms).
OH the reason why it's not an interface is because Go doesn't support generic interfaces, and may never.
Exactly. Go can’t do genetics methods. Prime has been using Go for a wihile now and can’t seem to have this conclusion
It's actually sad how many problems are created by the lack of support for genuine generics. Yes, it's slow to compile, but there's a reason most high level strongly typed programming languages support generics. They're super useful for lots of things, and certain problems can't be easily solved without generics. I'm not sure if it's funny or sad how much Go refused to support proper generics.
wrong, it does support generic interfaces
@@WoolieOG Incorrect. Go does not support generic methods on interfaces, which would be required to implement iterators.
@@alexlowe2054 It's not a "refuse to support" problem, it's a "nobody came up with a solution that met all the necessary criteria" problem
It reminds me of how D does custom iteration. First of all, there are both approaches, i.e. the push and pull approaches. A type that implements the pull approach is called a range. A range has the following members: empty (returns true if it’s done), front (gives you the current element), and popFront (steps forward). A "foreach (x; xs) { … }" is equivalent to "for (auto copy = xs; !copy.empty; copy.popFront) { auto x = copy.front; … }". Most algorithms, such as iota, map, filter, reduce, etc., are based on ranges. However, sometimes iteration needs nontrivial state, such as walking a tree. For that, the push approach is better. In D, you give your type a member function named "opApply", which takes a callback delegate as a parameter. The body of a "foreach" is transformed to an appropriate callback for you. When "opApply" calls the callback, the callback returns 0 indicating "go ahead", or something nonzero, which "opApply" is supposed to return immediately. The transformed foreach body returns non-zero on break, return, goto, etc. (not continue, though). The neat thing is, the D compiler as a switch that lets you see the code after such lowerings.
The opApply can be overloaded with different-arity callbacks so that you can use "foreach (x; xs)", but also "foreach (i, x; xs)" or whatever number of arguments the callback takes.
I have once written a tree type that has opApply walk the tree and provide an optional index, which is an array of indices that tells you which branch on the respective node was taken. Because "opApply" can re-use the index array, it can be allocated on the stack if the tree height is known in advance.
I think the reason for this is that for the easy models, Go already has and does what it is supposed to do. The design is literally meant to have full flexibility to implement support for everything else in Go. For anything the simple models support, you can already just use a loop with a channel. Works exactly the way you want and is cheaper. The whole idea of the proposal is to support everything that this approach cannot support.
The most important aspect of a language is readability. This seems extremely unpleasant to process mentally, like Rust.
rust is more readable than go in my opinion. it's kind of a combination of javascript and ocaml syntax which i find to be readable. in theory go should be more readable but it requires a lot of boilerplate and i find boilerplate in go to be ugly and hard to read.
@@matress-4-2323 as a beginner in Go, I have no problem understanding any codebase, idk what unreadable boilerplate you're talking about. Go is by far the most readable lang I've used, but then again I've never touched rust so far. But what I hear from others about Rust is everything but a nice readability experience
Less code is more readable that the mess that Go is becoming. Maybe true for junior engineers.
The function returning a function isn't a bad approach, but instead of the whole iteration and the yield function stuff, the iterator generator should return a next function like:
func backwards[T](slice []T) func() (int, T, bool) {
// setup everything
return func() (int, T, bool) {
// backwards access
}
}
oh are we bringing back the HR cut aways? hell yeah!
I do agree making go iterators use an interface does make sense.
fun fact we can sort any arbitrary datastructure in go by fulfilling an interface, so iterators would have fit right into this model.
In my mind, whenever Prime says "I wish they would have done it the *Rust* way". I can almost always just replace Rust with Java and it still works. Because he's usually just talking about interfaces anyway xD.
If you can replace it with Java, then you can replace it with C#. If you can replace it with C#, then you can replace it with BeefLang, - the ultimate language of all programming 🐮
This Go proposal still allows for vectorization optimizations. I wonder if they could do inlining as well (maybe when doing PGO) of the for loop's code block, which is probably non-trivial as the block is passed around as a function pointer with nested layers of closures.
Pull VS push. As always it's a tradeoff and depends on the situation. As an architect that's pretty much all I say every day. "maybe. It depends."
I think the explanation of push vs pull was rather confusing, if not confused.
Mentioning that JS array methods operate on the whole array before the next method happens is talking about eager vs lazy evaluation, both push and pull iterators can do both - though what Prime is probably getting at here is that it's a lot easier to do eager with a push iterator than lazy, while both are easy with pull.
The thing named iterator (or enumerator) from you're probably familiar with is a pull iterator, it has some "next" method you call to get either the next value or that it's done. A push interface is the opposite: you pass the iterator a "next" method that receives the items in the control of the iterator.
The trade-off is generally that pull iterators are much easier to compose, but depending on the language much harder to write (without some sort of generator syntax at least) as they need to keep internal state between calls.
Push is, in a way, really common in many situations you don't think of as being iterators at all, namely event listeners.
1
Basic text files are also without a standard for encoding, but a smart user will use UTF-8 and modern software will usually default to it. But before Unicode created a standard, it was very hard to decode text files. Unfortunately, they first created standards that would make every character 2 or more bytes. Because of this, many applications have to have an algorithm to try and determine the encoding.
So, being surprised that CSV doesn't have an official standard is mostly due to the encoding issue of the file that the data is stored in.
It's worse than that though. CSV makes no claims about when to use quotes or not, LF vs CR vs CRLF, escape characters, illegal sequences, unmatched quoting. Parsers and emitters are basically whatever "feels right"
RFC 4180
8:30 - "I don't know what n.b. is"
n.b. is "nota bene", or so FYI. or just "note: ..."
DUCK SOUND @25:04
Every time I hear Prime describe what a language should be I think he's describing C#. You can't make a language so simple that people need to write everything every time, you need to make a language simple enough so everything you need to write should already be written and provided in the standard library. That's C# and .NET!
I bet he writes c# all the time, but doesn't have the guts to tell us.
@@jimiscott, literally C# tsundere-kun
besides errors as values, and null safety o_O
@@Jason-xw2md You can do errors as values if you want.
What do you mean by null safety?
@@johnhershberg5915
You cannot as the code you call is free to throw exceptions. You can do a mix, but that isn't the same.
I want go to stay as simple as possible.
I agree that the syntax is not great. However, I disagree with the general statement that pull-based iterators are strictly superior than push-based.
The beauty of a push-based iterator is that any existing data structure traversal code can be converted to the push-based version. This is almost as simple as replacing your print function with the yield function in relevant places.
This is not so easy with the push-based, especially if you are using recursion in your implementation. Converting simple recursive algorithm into a pull-based iterator is not that simple.
Another factor is a performance. In a pull-based version you do end up with more branches since the implementation has to restore its current state on every iteration. This is probably less of an issue for simple traversal algorithms.
Another problem with the pull-based approach in C++ is that your loop state is managed in different places which makes it harder to reason about loop invariants.
Where pull-based iterators shine, indeed, is composability. You can easily implement various constructs on top of it, such as iterating two data structures in parallel. This would be impossible in a push-based version without buffering all elements from one of the sides.
So, back to the Go language proposal. Maybe it is in the spirit of Go’s simplicity.
imo I prefer C# iterators, they keywords yield return and yield break (and other syntax sugar) make it's iterators easy to read
Very similar to Python iterators and other languages.
Go seems to have made an overly complicated mess of a problem that has been clearly solved already.
@@101Mant Yeah, things like the simplicity of having an iterator that is not an iterable, simply by using a generator function/using a yield statement so that you don't have to load iterables that are unfeasibly large, while having to change *nothing* else, is a perfect example of why I just zone out all the hate Python gets and finish POC projects 10x faster than other teams. Haha.
That's generators, strictly: a way to implement the iterator interface. (Actually IEnumerable/IEnumarator)
Iterators are more about having an easy way to consume sequences (for each) than to define them, but really you want both.
@@SimonBuchanNz At least in Python's parlance, an iterator is a built-in class that takes either:
* an iterable (an object, like a list, tuple, etc) which is iterated over, calling __next__() after each iteration to retrieve the subsequent item in it, or
* a generator (a function that ends with a yield statement, and each time __next__() is called the function is called again to generate the next term in the sequence instead of loading a whole iterable when the class is constructed)
@@sophiophile that makes __next__ the python iterator interface, so a generator is still an iterator.
Nim:
iterator reversed[T](x:openArray[T]):T =
var idx = x.high
while idx >= x.low:
yield x[idx]
dec idx
var nrs = [1,2,3,4,5,6,7,8,9,10]
for nr in reversed nrs:
echo nr
Odin mentioned.
28:05 literally ran into the lack of standard today…
That's a pretty standard generator/coroutine/callback of callback/observer of observer pattern, it's just in the functional syntax
I really like that the new iterators in Go support defer.
I don’t have time to watch the full video so I might be missing context, but it seems really strange to me that channel syntax and go routine syntax wasn’t used. Really there’s not much going on here aside from having a go routine and channel that can swap back and forth without hitting a scheduler / mutex. Could have the syntax be like, out := go func(){ out
One of the main points was performance, because folks were using channels for iterator-like behavior. Channels have a mutex and involve the scheduler. Iterators are pure function calls and have order of magnitude less overhead.
In JavaScript you have push and pull, as well as both generators and a method interface. I think it's a'ight.
what the heck - i dont re-write the stuff ; i use proper golang structure so if i have it one place i can use it anywhere.
When I tested go, I felt like the things I missed were generics and iterators… it feels like I’ll be completely happy with go soon xd
Honestly the only thing that's missing for me is an easier way to deal with common errors. Zig does this excellently by implementing error returns as a seperate language construct rather than using multiple returns, which allowed them to easily implement operators to early return errors
So basically when Go catches up to real languages but with worse developer ergonomics.
Except they both seem half-baked and tacked on
I did honestly enjoy this video, as I generally dislike the screaming and acting “for entertainment/show”. Thanks!
Same for me. It butthurted me so much right after announcement. We were ok without it and now instead of well known N approaches of doing things we'll have N+1 approach. Already imagining zoomers and boomers arguing in the comments under every PR about doing/not doing sht though iterators
"historically, a C++ iterator would look like this:"
*shows a snippet of what range-for desugars to*
So much for the language designer.
Dude also has no idea why C++ iterators work the way they do. Anyone who wants to _actually_ understand this stuff and the tradeoffs being made should watch the talk "Iterators and Ranges: Comparing C++ to D to Rust" by Barry Revzin.
@@isodoubIet exactly, every time they need to compare how c++ handles this, they show how the expression is defined behind the compiler and not how it's actually used, it says a lot about the person...
"hey, let's compare it to c++"
- put the "real" c++ code in cppinsights and show the code to the compiler's eyes
And the standard library is so complete it's extremely unlikely you'll ever need to write an iterator in C++. So his entire argument is rather moot.
@@isodoubIetgiving this a watch, thx
@@justanothercomment416 And if you do end up having to write your own, it will automatically work correctly with all the algorithms in the standard library, some of which are flat out impossible to write in simpler iterator models. Alex Stepanov's STL is a work of genius; anyone trying to criticize it should at the very least understand it.
the iterator syntax, while it has sense to it, looks like pure chaos.
the whole function that returns a function that returns a function makes it look quite awkward.
Watching programmers argue, complain and make memes over complex unreadable syntax and calling human readable syntax "lame" is bizarre. I don't ever want to be a "programmer", I just want to learn how to create solutions for things I personally need done. I'd rather be on the Evan Czaplicki or Richard Feldman side of things. Thanks for reminding me why, genius system/low-level programmers. 🤣✌
Both generators and first class async are almost the same thing: a persistent scope/continuation.
If they made this thing a single concept from the language side, that would be one thing...
Generators are great, can be a lot cleaner than struct (because the struct IS the scope), but the way they designed it for Go doesn't make sense to me.
compared to other changes in GO (everything isn't internal), iterator involve very little ammount of go contributors and i think their decision are biased.
"if you can't decide, the answer is no."
they shoudn't standarize iterator. but instead provide a best practive guides.
Closures are just fancy objects, functions that take functions and return functions are just fancy classes
You're saying a closure is a higher order function? That's not right.
I thought I would hate it....I love it now.
The author admits himself to be imperative programming oriented. I think this is why the imperative variant looks easier to him. To me, the variant with yield is more straightforwards, maybe because I went through a functional programming (learning) phase. With functional programming concepts being built-into languages these days, I think new coders might agree
I don’t know how you can call Odin’s iterators simpler. I’ve always absolutely hated implementing pull iterators. I feel like Go’s approach is an order of magnitude simpler and less error prone.
@marcsfeh When composing with other iterators, it’s much harder (maybe even impossible) for the cleanup code not to run.
I have definitely messed that up in pull based iterators before.
@@hatter1290 That's not a problem of the iterator being a pull style, it's a problem of memory management though
Hey, Prime, random question: Why i cant watch 4k content on netlfix even if i have the 4k subscription and the HDCP protocol. Is there some sort of limitation because i want to watch 4k netflix content on my 2k screen but i cant so i have to make a custom res of 4k and then check netlfix to see that there is really 4k quality, which it is. Why just cant display 4k content on 2k display just like yt?
I'd have more sympathy for Rob Pike's statement about Googlers not being able to appreciate a brilliant language if I knew of a brilliant language he himself had designed earlier. As it is, it feel like several of Go's design deficiencies have more to do with Pike and team's unfamiliarity with non-C-family language features. For example, in ML sum types & pattern matching and type inference & generics work together brilliantly but it's still plenty simple to learn and use.
I appreciate Go's simplicity, but I can't shake the notion that it would have been a much better language while only a tiny bit more complex if it had some of these features, and further that if they had been familiar ML or similar that it wouldn't have taken them a decade to figure out how to do generics.
n.b. "nota bean" did chat meant to say "notez bien" 🇫🇷. Wtf is a naughty bean
It's from the latin "nota bene". On a beau être géniaux, on n'est pas le centre du monde.
they should have just called it yeet
The whole point of csv is that there’s no standard and it’s free for all.
In the end it usually works fine other than it’s very slow.
This just feels so unneeded to me ngl. The whole selling point of Go is being a grug-brained language and this is the opposite of that.
Seems like one of those weird things where you could just "not use it" in a different sense than how C++ is. It's not so confusing that you can't understand it without using it.
we really need to gate keep more
Any iterator ultimately stores state, rust just makes it explicit. The downside of this in rust is that it requires lifetems which are unfun. In go it would have been so easy to do with no additional syntax.
I guess we can call them "shiterators"
It's been a while I've felt dumb watching a CS video. Guess I have to study more iterators.
I expected a reaction video, prime.
Tonight on "When Iterators Go Bad!" (I'll show myself out...)
Iterators Gone Wild
There's a reason Go couldn't implement pull iterators with an interface. Something like
type Iterable[T any] {
Iter() Iterator[T]
}
type Iterator[T any] {
Next() (T, bool)
}
Seems like a very nice interface. The problem, and this was called out, is what if someone defines a type like `type MyType chan int`, and then implements Iterable on MyType? Now, if you for range over an instance of MyType, it's unclear as to whether you should get an iterator and call it's Next method, or whether you should receive values from the channel.
By making both push and pull iterators functions, you're making them unique types, rather than existing types that implement an interface, so this problem goes away.
Just prevent implementing the iterator interface on channels. This is the same as how in rust you cannot impl iterator on the Range struct or any other struct that is already iterable.
A neat way of doing this is to make a channel internally use the iterator interface.
If MyType implements the Iterable then clearly they meant to by iterable on MyType? Not sure how that would be unclear
I love gingerbill I will watch this.
hate the go iterators; javascripting my golang with worse looking syntax.
Rust also has yield etc, called coroutines (generators previously)
Nothing is real until it's stable
Could you create a video about Atlassian's new framework-agnostic, low-level dnd library called "pragmatic-drag-and-drop"?
Go iterators are very simple, people just overcomplicate them in their mind for some reason.
To parse CSV I use an ANTLR grammer/lexer/parser. It's so cool.
Lol do you think Dijkstra ever imagined that one article title would come to be a built in joke for programmers? 18:28
ODIN MENTIONED!!!🔥 WTF IS A BAD LANGUAGE DESIGN 🗣️🗣️
yeah, so who are we going to believe? The mighty creator Thor, or the guy trying to convince us of his cgi stach?
I despise this version of Go iterators. Far too hard to understand what's going on at a glance and very deeply nested. Go's designers aren't idiots so I'm sure there are very good reasons they're championing this design, but C#/Rust/Java got it right with an Iterator interface that you pull values out of.
CSV is for people who don't like regulation, you can make your own rules and carve your own incompatible path.
Iterators perform poorly in C++ and Java as well. In what language are iterators a good thing (does not create objects)?
I use c and lua because I like small numbers of tool rather large toolboxes. Do I get to be mediocre too?
I do wish they'd used an interface for iterators.
they are good actually. i've written like 5 slightly different iterators when making libraries. just give me a standard interface. thank you
My issue is: what does this let Go do that it couldn't before? This is a violation of what most people like about Go.
I like go a lot, and honestly I think they managed to find a solution that is very much in the spirit of go's simplicity. Like so much of the language, it requires a little bit more verbosity from the programmer, but it's clear what's happening under the hood.
That's the same cope Java devs use but atleast Java is actually readable
If the language itself is powerful enough (like Rust), iterators are a cake walk
What's wrong with CSV? Although TSV is better because you're less likely to use tabs.
That its a non-standard brings problems everywhere. Some are separated with a comma, some are separated with semicolons or tabs, some have quotes surrounding the values and some have them only when the separator is included in the value, some use haphazard json-typing inside the values, some have more than one header-row and so produce multiple tables, some allow newlines inside values, some use backslash for escaping and some don't, some programs produce a different syntax depending on locale and don't know how to read all of their own produced formats (e.g. excel). Standardization would do it good.
I haven't thought about it much, but does "the odin iterator approach" require that each slice be fully baked out into contiguous memory, or can it deal with sparse generation/population? cause I think other iterator approaches might be more generalizable.
is that difference the difference between "push" and "pull" iterators? I haven't heard those terms before.
not sure how much that matters in practice. I have used iterators and made generators a decent amount (about a decade), and I understand the state machine under the covers, but I haven't thought comparatively about different approaches to implementing them. in places where I've actually cared about performance, I've switched to a fully imperative model that's as stupid and ugly as possible, so I could be damned sure of the memory usage and control flow patterns.
but now that the subject is coming up, would be interested to see the differences.
summarizing a top-level comment here from SimonBuschanNz about push vs pull:
push iterators are in control of the data, and are easier to implement (ie loop over data and yield values), but do require that ability to yield values. push iterators are like event emitters or js .forEach, where the iterator controls the data and you pass it the next function, so it can call that with each element.
pull iterators can be more difficult to implement, because they usually need to keep state, but they can be easier to use on the client side. pull iterators are like calling a next function on the iterator when you want the next value, but you can choose to stop, skip by ignoring, sometimes reverse, etc. whenever you want.
generators by means of go channels are push model, the iterator produces data on its terms. generators in js are pull model, since you have to explicitly ask for values by calling next on the iterator (js does allow you to "push" values by using yield. however, generators don't choose *when* the client code runs, since they don't call a function which runs the content of the loop like .forEach doesz the caller chooses when, if ever, to accept the produced yield values, by calling next)
if you want to iterate over an infinite resource, like an iterator which always produces 0, you generally want a pull iterator, unless you're given a way to stop a push iterator (like your callback returning true/false) or you want it to go on forever
"I'm a language designer" 6:30
I will never understand why Go has such terrible syntax. It was freaking new. Everything the language is designed to do could have been done with really nice, familiar syntax, and it would have been an amazing language.
They tried to make it look like Python generators. That's the easiest way to think about it if you know Python.
C++ mentioned
Just code in a normal language like Java guys, that go snippet was simply too horrible to deal with
CSV tried to be so simple that it became extremly complicated.
5P and LTTM seething rn
why make such complex concepts and such heavy and complex function definitions (you have to write func 6 times) instead of making it simple like C#
yes, for sure
Rust unironically has far cleaner syntax than this mess.
@0:42 "for key,value := range myStruct" is 'super cool feature' only problem => it doesn't exist!
thought i might have missed so double checked and everything. (during which i see others saying this.)WTF
FWIW here's an field iter...
'''
import "reflect"
func Fields(v any) (seq[struct{string;any}]){
values := reflect.ValueOf(v)
types := values.Type()
return func(on func(struct{string;any})bool){
for i := 0; i < values.NumField(); i++ {
if !on(struct{string;any}{types.Field(i).Name,values.Field(i).Interface()}){
return
}
}
}
}
'''
Chat plays Go