Hacker News new | past | comments | ask | show | jobs | submit login
Loopless Code (2006) (jsoftware.com)
136 points by xept on Nov 26, 2021 | hide | past | favorite | 129 comments



The loops are still there, they're just implicit. That's how APL-family languages can be parallelised easily.

It's notable that the first J interpreter, while written in C, has a similar style --- it defines a macro to run a loop "implicitly", and then uses that throughout: https://code.jsoftware.com/wiki/Essays/Incunabulum


> The loops are still there, they're just implicit.

Obviously. I think the point is that loops should be considered "implemention details" at the compiler level, and us higher beings should be able to say what we want without troubling ourselves with them.


The article does have a heading "Examples of Implicit Loops", so the author acknowledges that the loops are there, but implicit. I guess "implicit-only-loops code" or "explicit-loop-less code" or whatever isn't as catchy a title, but I don't think anyone reading it expects it to be literally loopless code.


The current interpreter even has this! In fact, the meat of many (most?) functions is implemented using the (functions underlying the) primitives. It's kind of fascinating to skim through, if you're already familiar with array programming. The code goes way out of its way to reduce the impedance mismatch between J and C semantics.


Sean Parent has a good talk about no loops in C++. His primary argument is that typically a loop is an algorithm you’re applying (map, filter, reduce…) and the raw loop masks the algorithm.

https://m.youtube.com/watch?v=qH6sSOr-yk8


While I agree that a lot of loops could be better implemented as a map, filter or similar construct, there's still many loops where writing it as a loop makes it more clear what's going on.

For example, in our system we have orders. Each order has some order items as well as one or more invoice.

Due to reasons, some customers want to consolidate orders before processing them in our system. In that case all the items should simply be copied to the consolidated order, however for invoices we should accumulate values for the same invoice number and currency combo.

In addition, order items references the invoice they belong to, so we need to keep track of the new invoice id's so we can remap that reference.

Doing all this in a few nested loops makes the overall process very clear I think, each step in the loop being clear and logical. In that case, the loops highlight the algorithm I think.

I'm not sure how to implement the consolidation only in terms of map, filter and friends in a way which would be more clear.


> there's still many loops where writing it as a loop makes it more clear what's going on

I think that really depends on your language and what you are used to. For me, I would disagree.

> I'm not sure how to implement the consolidation only in terms of map, filter and friends in a way which would be more clear.

Tbh, when I read this, I'm thinking: is the data-structure wrong? Maybe you should have something like a "bundle" that contains orderItems and their invoice. It doesn't seem to make so much sense to keep an invoice ID in each orderItem. That makes sense in a relational db, but not when using objects.

I would model it like that (for lack of a better name than "bundle"):

    Order(bundles: Map[InvoiceId, Bundle(invoice: Invoice, orderItems: List[OrderItem]))
Now orderItems and the invoice-amount are logically bundled together and identified by the invoice-id. Then, combining two orders becomes really simple: combinedOrder = order1 + order2

No loops needed, simply treating the data as monoids.

Here's an executable example: https://scastie.scala-lang.org/DDIRbiC5TTuOxmkzEFxTqA

You might argue that it is not clear how that works and you are right for everyone who is not used to how monoids work. But everyone who is, independent of the language, will understand exactly what happens. On the other hand, someone who is not used to loops would probably argue that your loops are hard to understand.

It really boils down to what techniques one is familiar with.


I think I get your point, however the real world is muddy...

> I would model it like that

How does your model take into account orders where order lines do not map to an invoice, or which has invoices which no order lines references?

edit: interesting example btw, I do like a lot of aspects and it was fun seeing some "practical" Scala, haven't had a chance to dig in much. Much appreciated!


> How does your model take into account orders where order lines do not map to an invoice, or which has invoices which no order lines references?

If I understand you right, the latter seems easy: the list of orderItems would just be empty.

If there can be orderItems without invoice (why would that happen?) then I'd just add another field "plainOrderItems: List[OrderItem]" next to the bundles.

The rest of the code would not change.

That being said, there can surely be situations where modeling the data is not trivial. It's always exciting to model it in the best way to describe the problem at hand. And with this approach, sometimes you need to model different "views" to make combining things work.

> edit: interesting example btw, I do like a lot of aspects and it was fun seeing some "practical" Scala, haven't had a chance to dig in much. Much appreciated!

You are welcome :)


> If there can be orderItems without invoice (why would that happen?)

Because people mess up... In this case they'd then fix the missing invoice references post-consolidation.

Stuff like this usually happens either due to stress, lack of experience or training (summer temps are a menace) or just negligence.

In addition to our consolidation procedure, I've naturally had to write a procedure to undo the consolidation.

> then I'd just add another field "plainOrderItems: List[OrderItem]" next to the bundles.

That could work, would be more tedious when you want to operate over all items (we do this fairly often), but not terribly so.


Okay, interesting.

> That could work, would be more tedious when you want to operate over all items (we do this fairly often), but not terribly so.

Hm, not really. I've updated the code with a convenience method to see how it feels and I don't think it impacts the ergonomics at all:

https://scastie.scala-lang.org/QG1VYuFBQ4qRwfJSjq7bjA


Obviously I can’t see the business logic but I would express this (or what I believe I understand of it) as a fold that accumulates a map of structures which organize the invoice(s) alongside the orders somehow. I suspect that would result in a kind of “inversion” of responsibility that might be unfamiliar at first, but may well result in a clearer relationship between the data elements to someone new to the code (and may make refactoring a lot easier).


Can you post a (simplified) sketch of what your looping solution looks like? I'd like to give it a shot with map/filter/etc so we can compare.


It's very close to this, in C#-ish pseudocode, and barring errors introduced by my fried Friday brain. The duplicate invoice lookup isn't a linear search but I wanted to keep it simple. In reality order items have various sub-items, but lets ignore those.

It's not super elegant, but in my mind it's straight forward and should be easy for my colleagues to jump in and understand. Though I'd be delighted to be proved wrong.

    var dstOrder = new Order();
    var invoiceMap = new Dictionary<Invoice,Invoice>();
    
    for srcOrder in SrcOrders do
    {
      for srcInvoice in srcOrder.Invoices do
      {        
        // compares using invoice number and currency
        var dstInvoice = dstOrder.Invoices.Find(srcInvoice);
    
        if (dstInvoice == null) {
          dstInvoice = dstOrder.AppendInvoice();
          dstInvoice.CopyFrom(srcInvoice);
          invoiceMap[srcInvoice] = dstInvoice;
        }
        else 
        {
          dstInvoice.Amount += srcInvoice.Amount;
        }
      }
    
      for srcItem in srcOrder.Items do
      {
        var dstItem = dstOrder.AppendItem();
        dstItem.CopyFrom(srcItem);
        // map old invoice reference to new (possibly consolidated) invoice
        dstItem.Invoice = invoiceMap[dstItem.Invoice];
      }
    }
    
    dstOrder.Save();

    return dstOrder;


some of this is a little unclear. in particular how the source invoices are identified and mapped and therefore what invoiceMap really looks like. but here is a group/sum in a madeup datalog dialect:

reconcile(dstId, amount) :-

  Orders(srcOrder)

  Invoices(srcInvoice, srcOrder)

  unique(dstId, srcInvoice.id)

  amount = {
     sourceInvoice.id = dstId,
     sum (srcInvoice.Amount)
  }
that produces invoice/sum pairs. Orders and Invoices are your source relations. unique produces a set of unique source invoice ids, which drives the cardinality of the aggregate. the block notation is a scoping construct encapsulate the set cardinality changes.

not going to claim that this is inherently more readable or captures all the subtleties. but you can imagine that terse expressions of intent like this are more accessible to the reader and less error prone. if not, I should do a better job.


Sorry, I was in a bit of a hurry. An Invoice has essentially these fields:

    id
    InvoiceNo
    InvoiceDate
    Amount
    Currency
The id is the unique id in the DB. For the purposes of consolidation we consider (InvoiceNo, Currency) as a tuple. For brevity I didn't include the failure mode of mismatching invoice dates, that can be checked in advance anyway so not terribly relevant.

The invoiceMap maps instance to instance, or the unique id to unique id if you like.


I guess my answer depends on what kind of DB this really is.

If the app is running off a SQL Backend, you could probably do this with some well thought out queries, however whether doing so in an ORM/ActiveRecord Environment (which is what this looks like) will be beneficial might be another matter.

For instance, if this was SQL and a Micro-ORM was involved, I'd instead try to grab all the data in one pass (With the right ones that's fairly simple,) Calculate my Insert set from that, and write out the new records. In that case though, there'd probably still be some form of LINQ/Looping, both on the level of business code, as well as under the wire. It would be more performant for sure, but to your point, IDK whether it would be more understandable


Map reduce and filter often are much more readable, but then sometimes a loop is clearer. I often see this when I use resharper's (a c# tool) auto refactor a loop into a linq statement , it can become unreadable.


Readability depends on both the underlying algorithm and its expression in code. Because LINQ has to work in existing languages, it can't express loopless algorithms as well as languages like J that have syntax designed to fit the style. You're probably also putting it at a disadvantage with the automatic translation, as you'll write different and cleaner array code if you approach the problem with array operations in mind.

The author is claiming (and I and many more practical-minded programmers agree) that in J, explicit loops are rarely needed, and usually not even helpful. As the J notation is designed for loopless programming, it has the same kind of bias, against loops. But knowing how J approaches things can be valuable. Most likely, many problems you think are unapproachable with map and reduce can be solved easily by knowing the right techniques.


Plus they may have hidden performance costs - or benefits. They may have function invocation and memory layout costs, but they may also be parallelised and optimized transparently. A regular for-loop is super efficient in terms of memory layout / access but it's by definition singlethreaded.


Yes, as always, it depends. The typical problem with array operations, e.g. in Fortran, is how well they're "scalarized", and the possible cost of not doing copy elimination over subroutine calls, for instance.

Presumably the exact semantics of a for-type loop depends on the language, but surely the C standard doesn't prevent auto-parallelization (including with SIMD vectorization, modulo arithmetic rules). Loops might also have OpenMP or other annotation.


> If the rank of the verb's operand is smaller than the rank of the verb, the verb is applied to the entire operand and it is up to the author of the verb to ensure that it produces a meaningful result in that case.

This instantly brings back all my frustration with getting broadcasting in numpy to work like I want / expect. I love getting the right dot products to work between an array of matrices and an array of vectors, but I don’t do it often enough to remember how, I have to slowly re-derive the incantation every damn time.

> J does contain while. and for. constructs, but they carry a performance penalty

Question - what is the state of the art of functional programming for performance? My experience in JavaScript, Python, and C++ is that using loop-hiding pure functional constructs is difficult to impossible to optimize, often much slower than explicit loops, and worse that it’s harder to refactor when you realize your nested loops are inside-out from what they need to be. I want to use functional more often, but I feel like I hit roadblocks in practice.


I find rank in APL/J to be much easier to understand (and much more powerful) than broadcasting in numpy.


Part of the reason for that is because APL-family languages support this feature as a first class citizen and are designed with them in mind. Numpy has to, for better or worse, work within the confines of the Python grammar.


From the title I was sure this was going to be about the branch forward only pattern used in the Gripen, that John Carmack has briefly written about. In that pattern, there's a top-level loop, but other than that, no looping constructs or other backward-branches are permitted anywhere in the codebase.

* http://lambda-the-ultimate.org/node/5362

* https://news.ycombinator.com/item?id=22192656


A popular example given for this is often pcap/tcpdump filters— for perf reasons they have to be executed by the kernel, but because they're an untrusted user "program" being run in kernel space, they can only ever jump forward, ensuring an upper bound on their runtime.

See: https://en.wikipedia.org/wiki/Berkeley_Packet_Filter

That said, it looks like as of Linux 5.3+, there are certain bounded loops that are permissible: https://lwn.net/Articles/794934/


This sounds very interesting.

What exactly would be a backward-branch? Are function calls allowed (I feel like they have to be)?

Are there any code snippets for this style?


According to Carmack subroutine calls are also disallowed. See https://web.archive.org/web/20210226152857/http://lambda-the...

This sounds very much like programming a traditional Programmable Logic Controller (PLC).


Seems curious to ban functions. If you ban function-pointers then you can statically ensure there's no recursion (including mutual recursion). At that point, all function calls are in principle able to be inlined, so any backtracking is merely a compiler-level implementation detail.

iirc, OpenCL C does something similar, banning function pointers and recursion (including mutual recursion), although it does so for different reasons than this pattern.


Well, a flight control computer is not too different from a PLC.


It seems to me that this would be best ensured by writing the codebase in a custom language the compiler of which would put a loop around the whole code but the language itself wouldn't have loops. This could be yet another instance of "patterns mean 'I've run out of language'".


No Stinking Loops! http://www.nsl.com


Genuine question, what would be the idiomatic way of doing an impure operation multiple times in J/APL ? (e.g. if writing an interpreter loop)

I was wondering if it's doable without the while. and other constructs (which honestly feel like plugged artificially in J, even syntax wise)


The idiomatic way would be to exit the array paradigm and use an imperative (while.) or functional (recursion) method. APL and J both have "repeat until convergence" functionality, which stops when the same result is returned twice in a row, but to use this you'd have to artificially create a result that changes each time.

When designing BQN I embraced the limited nature of array primitives, so that most primitives can only implement efficiently parallelizable programs and none of them can perform infinite loops. Flip this around and you get guarantees: if you create a function by composing primitives you know it will halt, and if you avoid using modifiers in complicated ways it's easy to prove good sequential and parallel bounds on the runtime.[0] Although BQN has no tail recursion (J also doesn't; Dyalog does), it's possible to implement loop functionality that uses only logarithmic stack space in the number of iterations, with low overhead (I just measured 30ns/iteration in CBQN for a simple incrementing loop).[1]

[0] https://mlochbaum.github.io/BQN/doc/primitive.html

[1] https://mlochbaum.github.io/BQN/doc/control.html#low-stack-v...


in K, there is an adverb form which is essentially equivalent to a while loop: apply a function or composition to a value repeatedly as long as a second function or composition of that value yields true.

It's basically an "escape hatch" when an algorithm cannot be cast into any more specific pattern, like iteration, a fixed-point, a reduction, etc. In practice it is needed very rarely.

There's a complete list of adverb forms for k6 here: https://github.com/JohnEarnest/ok/blob/gh-pages/docs/Manual....



Why so much hate for loops? Loops are just a nice notation for some computational constructs. Sometimes they are the clearer way to write an algorithm. Very often an algorithm becomes clearer when written in explicit loop form than in "vectorial" notation.

Loops do not need to be artificially slow to discourage them. Any modern programming language should be able to recognize the construct and compile it in the most efficient way. Saying that you must avoid loops because they are slow is a failure in a particular programming language, not in the concept of loop. After all, all languages managed to implement loops efficiently many decades ago.


I think the idea is that a loop in many cases doesn't express your intent. What you might want to do is to "set the property 'foo' to 'bar' on all items in an array".

What you might write is "create a variable and set it to zero, then iterate that variable as long as it's smaller than the number of items in the array, and do this between each iteration: take the item at the variables index and set its property 'foo' to 'bar'".

Of course any programmer will instantly see what's going on, but that's not from clarity but from prior experience and familiarity.


That's only true for languages that don't support looping over an iterable directly. Or if you need the index while iterating too.

Python, Rust, Swift, C++, ObjC and more all support the equivalent of a foreach loop today. That perfectly shows intent


True. It is possible though for a language to avoid these iteration constructs as if they are needless plumbing as well. Consider the elegant type system of the XQuery language:

In XQuery, the root type is a flattened sequence. Flattened means not just that [1, [2, 3]] is flattened to [1, 2, 3], but also the a single object 42 is equivalent to a sequence of one object [42].

This means that if I write:

  declare function local:prefixName($name)
  {
     fn:concat("prefix-", $name)
  }
I can invoke it with a single name or a sequence of names without any explicit iteration.

It has drawbacks but using XQuery certainly highlights how much code we normally waste dealing with "things" and "sequences of things" separately.

(Than again working with XQuery also highlights how much code one can waste dealing with XML namespaces :)


Admittedly it's a worst case scenario, but the broader point still holds even with a foreach. You are still picking out each item individually instead of operating on the collection which is what you intend to do. You write "do this for every apple: peel it", instead of "peel the apples".


apples.forEach(peel) is pretty clear, and the distinction is actually important for some operations which could conceivably be applied either to the whole collection or to each item in the collection and thus could be ambiguous in plain English, like “gift wrap the apples.” That could mean giftWrap(apples) or apples.forEach(giftWrap).


Here's another comparison: peel and add to mixer fresh apples only.

    let mut mixer = Mixer::empty();
    apples.for_each(|apple| {
        if apple.is_fresh() {
            mixer.add(apple.peel());
        }
    });

    let mixer = apples
        .filter(Apple::is_fresh)
        .map(Apple::peel)
        .fold(Mixer::empty(), Mixer::add);


They're both pretty clear but I'd prefer the second one (if you used collect instead of fold)


I chose Rust syntax because it allows both chaining and using methods as functions, but avoided Rust-specific features. In addition to `collect` this example is missing `.into_iter()` that both examples would need: I interpreted `apples` as an iterator instead. Otherwise the first example would be clearer with a for loop.


I love me some forEach methods! It's still one off from completely avoiding the loop-think though. A syntax like "apples.'peel()" (where peel is a method on the individual apples) is what I imagine.

As a PS, I think the parent comment was referring to a (corresponding) "for apple in apples: peel(apple)" syntax though. The main point was that you don't have to bother with the manual index iteration as in my worst case example.


Well said! And when this clarity is lacking in a codebase, damn do I find it incredibly frustrating! Like, can’t you just tell me whether you are trying to operate on the items of the collection or the collection as a whole??? Why did you need to adopt this ambiguous formation??


> That's only true for languages that don't support looping over an iterable directly.

Even with loop-over-iterable semantics, explicit loops obscure intent compared to comprehensions and map/reduce/filter/etc.; they are better than C-style loops which in turn are better than conditionals + gotos, but that doesn't make them good at expressing intent in many common cases.


Agreed, and based on the list here: https://en.m.wikipedia.org/wiki/Foreach_loop the only popular language without for each is C.


And even then, a decent number of data structures provided in headers on *nixes provide for each macros.


there is also a pretty big difference in what a compiler can do if we express the computation as a set of operations that transform the sets rather than a little rats nest of mutations and jumps.

if you wanted to vectorize the set/jmp version, you'd be trying to tease apart a problem which is undecidable in the limit. vectorizing the transformer version is trivial.


There’s also the reverse: writing as a loopless operation forces the programmer to write code that can be easily vectorized.


In python, the way you write this is more similar to how we express the intent, using dict/list comprehension. But the thing is, for anything more complex that your example, the comprehension notation is harder to read and make changes if you are not really used to it


Pythonic for loops are more or less `for item in list: <block of code>`. Sill no manual management of array indices.


This seems perfectly clear.

  for item in array:
      item.foo = bar


   array.'foo = bar
Is clearer. Or, at least, would be, if one were accustomed to the (fictitious) adverb '.

There's no need for all the ceremony around specifying "for x in y: <some function describing what to do with x>"


That does look pretty nice for this example. I think the problem with it is that introduces too much new syntax that you have to memorize. The beauty of a for loop is that you can do _anything_ with it. And it all works with the same syntax.

  for item in array:
      item.foo = bar

  for item in array:
      item.foo = item.foo * 2

  for item in array:
      my_bar_func(item.foo)
If you special case certain uses of a for loop and add language syntax around it, of course you can make code terser. But it's nice to rely on a small set of very generally useful syntax.


> The beauty of a for loop is that you can do _anything_ with it.

That’s also the curse: a generic loop can be almost anything (looping over an iterable can't quite express anything, but it can do a lot), so seeing it tells you nothing.

C-style loops can do more (literally anything), and loop-over-iterable syntax has been generally preferred where it can be applied because it tells you more (even if only a little more) about what the block of code is doing up-front.

Constructs that are more specific than loop-over-iterable can be correspondingly more expressive.


depends on how many different types of loops there really are. now _that_ would make an interesting article.


It isn't. "For x in y" is literally what we say when speaking as humans unlike that quote thing.


I don't find that to be a convincing argument, because it suggests we should write "foo applied to bar" instead of "foo(bar)" in your code. It's just notation, just because it's new, doesn't mean it's bad.

There's maybe an argument to be made about there being _too much_ notation to learn, but notation makes sense for fundamental concepts (which loops clearly are).

Notation is important. It condenses thought and refines the concepts, making them more visible. In fact, some valid mathematical theory came about from figuring out proper notation and then playing around with it, sometimes in non-rigorous (albeit intuitive ways). e.g. consider how physicists wildly move dx and dy around in differential equations (much to mathematicians' dismay)

Ancient greek geometry textbooks were written in long form: "the square of the hypotenuse side is equal to the sum of squares of the other two sides". We can all agree that the "a^2+b^=c^2" notation is better.


That's an awkward way to speak.

"Take each of my gizmos, then put it in the gizmo box"

    for gizmo in gizmos: gizmo.put_in_box()
vs

"Put all my gizmos in the gizmo box"

    gizmos.'put_in_box()


The second code doesn’t put all the gizmos in the gizmo box. That’s your intent, but not the reality. All that syntax says is apply function to all elements. It is syntactic sugar for the code you put above; it’s a for each loop.


Well, the argument is about expressing intent, so I guess we agree.


Your first statement is unambiguous.

Your second sentence is ambiguous. It could mean

1. Put all of my gizmos in a single gizmo box, together, OR

2. Put each of my gizmos in its own gizmo box


Are you talking about the pseudo code or the readable sentences? I hope my point comes across even if there's some linguistic nuance I'm missing.


I'm talking about the readable sentences. The one you labelled as "an awkward way to speak" was unambiguous. The phrasing it seems you preferred was ambiguous. Personally, if I have to write the sentence down and the consumer of the sentence can't ask questions to clarify, I prefer the unambiguous wording. Ie, say exactly what you mean or you may not get what you want.

> A wife asks her husband, a software engineer, “Could you please go shopping for me and buy one carton of milk, and if they have eggs, get 6!” A short time later the husband comes back with 6 cartons of milk. The wife asks him, “Why the hell did you buy 6 cartons of milk?” He replied, “They had eggs.”


Or “paint all my gizmos red”.


I think you will find that those “humans” you’ve been hanging out with are all programmers or mathematicians. “Humans” don’t talk like this. Most of them have never heard of set theory.


Maybe it's just my mother tongue, but "pour chaque entrée dans la base de données, prends la moyenne et affiche-la" sounds pretty ok (literally in English, for each entry in the database, take the average and print it)


Not necessarily. The later expression might be more common though:

"For every item in this truck, remove that item and put in in the warehouse."

"While this truck is not empty, keep unloading items to the warehouse."


> "For x in y" is literally what we say when speaking as humans unlike that quote thing.

What about:

"Can you unload the truck?"

The current context gives us implied knowledge that we're talking about the items in the truck and where it's getting unloaded to, such as a warehouse if we're near a warehouse door. We only expand on that verbally when we need to such as "Can you throw out the empty boxes in the truck?".

I think this is why programming is nonintuitive, you have to include important steps with very precise words that often go unsaid. Even "throw out" wouldn't convert to the correct intent if a computer executed that statement as is. You would end up literally throwing the empty boxes out of the truck instead of putting them into a dumpster or garbage can (the likely intent based on this example).


> all the ceremony

It's not _that_ verbose -- the trade off in clarity is pretty stark IMO.

In practice many people move between languages frequently daily. This syntax and semantics it provides are fairly different from that of popular languages.


With numpy, this would just be item.foo [:] = bar. Much more readable to me - the whole «for» thing is superfluous.


Indeed. Also, this notation was lifted straight from Fortran 90 (which itself got it from Matlab, Algol, and the primitive slicing that was possible in older FORTRANs). The idea that a concise notation is helpful for the concept of “put a thing in all the buckets of an array” really is quite old. It does make a lot of sense.


Neat trick. Didn't know about it.

In pandas it would be item.foo = bar, _as long as_ bar isn't something you can iterate over.


Numpy is all about this. E.g y = np.sin(x). X and y are arrays and you can think of them as «all values of x and y» or «the physical variables x and y»


These comments from people who don’t write software and only use programming for data science. Numpy is superfluous for anyone that doesn’t need it.


The PLT position on this is that loops are too expressive. We'd like to use the least powerful tool that can do the job. Loops can express all sorts of incorrect versions of the intended algorithm. It's preferable to try to identify the reusable structure in the algorithm (it might be a "scan" or a "fold", for example) and implement that once and for all. We then provide a simple interface to that algorithm, without any footguns.


That's like saying a bag is too expressive because I can put all kinds of things into them.


> That's like saying a bag is too expressive because I can put all kinds of things into them.

That's entirely accurate. A bag is "too expressive", in that sense. Sometimes you want to constrain the kind of container so you don't put the wrong kind of thing in there.

Another analogy: types. "Any" will let you use anything, but sometimes you want to say "only allow Integers here".


What I intended to hint at is that a bag being expressive is not the issue. Yeah you sometimes want to label the bags, or make special purpose bags. But bags as they are are not too expressive. Them being expressive allows for all that other stuff. Their core utility is what we're after and the rest is secondary support and should be treated as such or else we get lost.

If we act like we're not actually dealing with bags at a fundamental level, we might mislead ourselves and spread the belief that the rules and labels around the bags are what matters. No, they help you (temporarily) to use the bags in a specific way in some context.

In fact, computers are extremely expressive, (bag like) things. The constraints are auxiliary. I think it's important that we keep reminding us what programming is and that it serves a direct purpose, while the bureaucracy is a support structure at best.


> The constraints are auxiliary.

Constraints enable reasoning, both automatic and manual.

Loops fit the single-core CPU hardware paradigm extraordinarily well. As soon as you start targeting modern hardware, more constrained recursion/iteration schemes start to buy a lot more in terms of both developer time and compiler complexity.


Agreed that computers are extremely expressive. I think the constraints are necessary though, and not just bureaucracy. After all, what are bugs (especially harmful bugs) but a failure to constrain choices?


More practically, a List is too expressive, if all you need is a Set - which is true and generally good advice to use Set instead of List, if possible.


The "if all you need" part is very important here. Without it the previous sentence makes no sense at all.

In addition to that, the Set is more "expressive" than the list in the right context. Or rather it expressing things more directly.


From what kind of programming hellhole do you come from, where using Set instead of List is good advice? Lists are very natural constructs for computers, sets aren't.


Sets specify a particular interface but not a particular implementation or backing data structure. You can substitute different implementations (including ones backed by lists, vectors, hash tables, and others) depending on your particular needs.


A set implies a uniqueness constraint that a list does not.

Generally a list is ordered by contract, whereas whether or not a set is ordered depends on the implementation.


Sets generally will give you constant time lookup where lists generally will not.


> Why so much hate for loops?

I don't think there's any _hate_ for loops, however, they are a very common pattern, so it's not unreasonable to argue it can be further refined/abstracted.

Most people agree that "for x in y: foo(x)" is an improvement over "for(int i=0; i<=n; i++) {foo(x)}". It's not wild to argue that "foo' x" is an improvement over "for x in y: foo(x)"


> Most people agree that "for x in y: foo(x)" is an improvement over "for(int i=0; i<=n; i++) {foo(x)}"

Well, I don't.

The second version is much clearer to me, for example, if I want to be sure about the running time of the code. Will it take one second or be instantaneous? The first example hides the length of y. I favor a programming style where all loops have clearly identifiable bounds. Using "iterators" hides these lenghts and makes the code much harder to reason about.


> The second version is much clearer to me

Clearer? Did you notice the second version contains a bug?

The first version makes it easier to spot said bug, because it's more concise and with fewer parts where you could make a mistake. Note that even in this very short piece of code it's easy to miss the bug; imagine in actual loops!

You have no guarantees about the running time of the code in either case, not without delving into the specifics of your programming language.

> I favor a programming style where all loops have clearly identifiable bounds. Using "iterators" hides these lenghts and makes the code much harder to reason about.

I find the opposite to be the case. Explicit loops with indexes make off-by-one errors likely, which is one of the most common kinds of error. It's impossible to make that kind of mistake with a foreach or map-over-this operation.


> Did you notice the second version contains a bug?

Whoops - in my defence, I haven't coded in C in a very long time. I'll leave it there, since I think it makes an (unwittingly) good point.


This is mostly a matter of perspective. An off by one error may go harmlessly unnoticed at worst, or trigger a harmless segmentation fault at best, in which case it's very easy to find and fix the silly problem.

On the other hand, code that hides things tends to be much harder to debug, when it invariably fails. Why is my program suddenly gobbling 10GB of memory? After several hours of profiling and debugging: ah, here's this particular combination of constructors that allocates and frees a stupid amount of memory for no reason.


> An off by one error may go harmlessly unnoticed at worst, or trigger a harmless segmentation fault at best, in which case it's very easy to find and fix the silly problem

Yes, but even better if you don't write the bug to begin with. It can also fail harmfully, like failing to trigger a segfault but producing the wrong computation, resulting in sending the wrong amount of energy to the X-Ray machine, the wrong amount of money in a bank operation, firing the wrong number of missiles, etc.

> code that hides things tends to be much harder to debug, when it invariably fails

In the general case, I agree. In the case of foreach loops (or maps or folds) the good thing is that they are well understood abstractions, not something your fellow coworker wrote and that you must understand from scratch. They are common idioms, just like you don't debug (under normal conditions) how a for-loop with an index works internally.


> An off by one error may go harmlessly unnoticed at worst, or trigger a harmless segmentation fault at best

Or be used in a remote code execution attack. So "harmless unnoticed" is FAR from the "worst" case.


Yeah, I had to reread that because I'm pretty sure "silently wrong" is the worst case, and not harmlessly so; I would 100% prefer segfaults, which let me actually fix the problem or at least know something's wrong.


The analogous for loop would have you compute n from the size of y. You still don’t know, statically, the length of the loop in that case unless you statically know the size or length of y.

It has the same problem either way. Similarly, if you always know the size of n in the C style loop, then you would always know the size of y in the Python style loop.

The Python loop just has the benefit of being shorter (by a line and a few tokens).


Folks, folks! Can we not downvote loops at least? All we want, I think, are useful tools. For goodness' sake, isn't it obvious that loops are a useful tool?

Not a compulsory tool, not a forbidden tool, not a foolish tool, not obviously the correct tool in all situations, possibly not something that needs to be mainstream in every work flow? Sure, all of those. But ... we're downvoting loops now?


If these two examples are intended as alternative versions of the same idea, the value of n would be something like y.size().


Both hide the length…


Loops imply that the order matters when it doesnt.

When order does matter, loops are quite enjoyable


Thanks, this was a good post and worth skimming through all the other nonsense to get to


Loops are great if your programming languages supports iterables/iterators/generators (also async generators) like in js/ts for example.

Especially generator-to-generator combinators ie. [0] gives terse, transducer expressiveness over computation on finite/infinite streams, arrays, etc. (all iterables). It's easy to compose, jump into/out-of for-loops if needed for arbitrary yielding (ie. conditionally yielding multiple items, skipping some, halting etc); `continue`, `break`, nesting, yield, yield from (yield*), normal code in for-loops is very intuitive and terse, creating pleasant, understandable code.

Recently I had a chance to break down complex pipeline in finance project into small steps as generator-to-generator transducer functions (can you call generator-to-generator functor a trasducer?), higher order for steps that require extra inputs, leaving all pipeline pure and easy to reason about. Typescript was really helpful doing hard work where each step somehow modifies the input - leaving final result typechecked together with spotting bugs in intermediate steps ie. on their dependendency etc. really nice stuff.

[0] https://github.com/preludejs/generator


> Why so much hate for loops?

Well, for me, because, compared to other notation, they usually obscure intent.

> Loops are just a nice notation for some computational constructs.

But... they're mostly not.

> Loops are just a nice notation for some computational constructs.

Sometimes, but even then if a language has decent abstraction features that’s usually best for the low-level implementation of a generic algorithm, which you should never have to see when actually applying the algorithm to concrete types. True, if you don't have a generic implementation available, YAGNI often argues in favor of a concrete one for the immediate use case, so generic implementations separated from immediate concrete use isn't the only place to use explicit loops.


Not hate for loops, but if you replace them with, say, array operations, you've removed the need to prove your loops correct -- or removed a possible source of error from getting them wrong. I guess what's clearer is a matter of opinion.

Reconstructing what was meant at high level from arbitrary loops is surely intractable, but could only be a property of a compiler, not a language. Should a compiler like GCC necessarily recognize a matmul loop nest and replace it with a GEMM call? XL always does -- which can make investigating it's optimization annoying -- but then it doesn't actually recognize GEMM.


Oh but in J loops exist, they are mostly implicit in the operation.

So it is not “loopless” per se, as the sigma notation for summation indicates a loop in a single construct, and it “can be written away” using Einstein’s notation.

That is how it should be understood, imo.


For me at comes down to telling "how" instead of "what" to do.


if you're not writing explicit loops then you must be doing something else. is that something else interesting? you'd have to dig in a little bit to find out.


I don't use loops because they force you to use side effects in your code.


When I saw the title the only method for repetitive code I could think of to replace loops was recursion.


I think this is what ATS does, it does not have loops, only recursion.


I've found this tends to happen in my Rust code too, and even in my JavaScript lately (though sadly it has performance costs in JS). It's true that 90% of loops in practice are just for processing collections, and that can be better served in most languages by using a harder-to-mess-up construct that's designed for the purpose.

It really does almost feel like an extension of the goto trajectory, since loops themselves were one of the purpose-built constructs designed to cover specific, common uses of goto.

I wonder if we'll see a "control-flow considered harmful" one day (this is a joke... mostly)


I usually avoid loops. Guess I got sorta hooked by this new age functional school.

But recently realized even layman brains on drugs can understand some loop and goto statements:

"Eat Sleep rave Repeat"

Sometimes they are just the best to write logic.


'and then he discovered loops' classic early apple interviewing story

https://www.folklore.org/StoryView.py?project=Macintosh&stor...


Isn't all this popularized in SQL? And (at least for me) written with a much clearer syntax.


I recently was wondering how to write a program, in SQL, which generates consecutive integers - 0, 1, 2, 3... - up to an arbitrary input value. Only standard SQL is allowed, and familiar constructs are preferred - the code should be understandable to the maintainer.

In J it's i. <n>, like this - i. 5 produces 0 1 2 3 4 .


SQL is not turing complete, at least not without recursions. But you will still get very far when it comes to wrangling data. So far that you may not even need another tool for that purpose.

What do you want to use that list for?


I think their point was that J and SQL target different domains, and each will be stronger in its respective domain. If I want to join two tables, filtering on some value, sorting, and viewing the first ten results, SQL will likely be the cleaner syntax. If I want to apply a polynomial function to a list of values, and calculate the standard deviation of the result, J will be much cleaner.

There's some similarity, at a high level, with how they work. But it doesn't really make sense to say one is, overall, better than the other, because they don't solve the same problem. It's like saying a hammer is better than a screwdriver.


  WITH RECURSIVE num AS (
      SELECT 1 AS id
      UNION ALL
      SELECT id + 1 FROM num
  )
  SELECT * FROM num LIMIT 5;
SQL:1999 IIRC.


Admittedly though, recursive CTE's like that are a bit of a minefield in practice. It's easy to confuse the query optimizer after chaining a few of those together, or to outright hit a recursion limit (32,767 in SQL Server).


A past related thread:

Loopless Programming - https://news.ycombinator.com/item?id=21278790 - Oct 2019 (122 comments)


BPF didn't have loops in the beginning either, but since Linux 5.3 it supports bounded loops as that seems to make programming a lot easier in many cases.


I will always upvote APL/J.


HN's top-voted comment at this moment begins: "Why so much hate for loops?" The item cited as 'hate for loops' is an introduction to 'loopless' programming in J, a universally admired language developed by Iverson. Whether LINQ or SQL etc. are also examples of 'hate for loops' or not ... I'm done.

For every web site there's a moment when user comments need to be declared 'not worth the pain of sifting through.' HN has crossed that threshold for me.

[Edit: My point is that we are crossing a sort of event horizon. Short-sighted micro-kvetching is becoming the largest single focus of user comments, upvotes, etc. on this site. Perennial examples: one-indexing vs zero-indexing; OO 'vs' FP; whitespace. You can list more yourself.]


> For every web site there's a moment when user comments need to be declared 'not worth reading.' HN has crossed that threshold for me.

So what you're doing is the exact same thing, but for HM ITSELF? /j

I don't think think the comment was unwarranted in this case, because the article makes no attempt to explain why what it says is a it admits is a building block of traditional programming is a bad idea. It just says "j does loops bad, do this instead"


> HN's top-voted comment...

...spawned a really interesting conversation on the PL design-tradeoffs of various special-purpose iteration/recursion schemes.

> J, a universally admired language developed by Iverson.

Even in hard-core PL communities that seems like a reach. Lots of PL courses exclude APL/J, which is a pretty strong empirical proof-point against universal (or even strong majority) admiration. Outside of PL enthusiast communities, array PLs sit somewhere between "niche" and "esoteric".

I think there is a lot to admire in APL, J, et al. They clearly had far-reaching influence. But there's also a reason that the dominant reaction is closer to "um... heh" than admiration. There's no need to over-state the case here.


This comment should reply to the comment about which it kvetches, instead of being a top-level comment.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: