Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

One of Swift's strengths is that most of its features really make sense if you read them, even if you aren't aware they existed. The entire language is optimized around readability. (No, I'm not counting the function builder fiasco.) For example, you may not know that Swift's loops support where clauses:

  for i in 1..<10 where i % 2 == 0 {
      print(i)
  }
But you can immediately understand how it works, because it's consistent with the rest of the language and reads well.


Any claim of the form "this language is magical! every statement is self-evident" has been bunk since the dawn of programming. The people making such claims are always already too close to the language to realise their own confirmation bias.

So, feedback, from someone whose exposure to Swift consists basically of this article but has eaten a metric crapload of many other braced languages over the decades.

What I can tell is that the programmer wanted to print 2,4,6,8. So the intentional readability is there. However when it comes to understanding how it works (i.e. the language mechanics delivering the intuitive reading) I had lexical concerns. I couldn't immediately tell whether the where-clause acts like a guard predicate in the for-statement or an unbraced if-statement i.e. equivalent to

    for i in 1..<10 {
        next if !(i % 2 == 0)
        print(i)
    }
or

    for i in 1..<10 { 
        if (i % 2 == 0) {
            print(i)
        }
    }
or even (let's hope not)

    if (i % 2 == 0) {
        for i in 1..<10 { 
            print(i)
        }
    }

and in a similar vein the lexical scope and binding of i was not clear to me; some languages bind the iterated variable at the level of the statement, others inside the iterated body (which may even be a closure for all I know), and this has implications particularly for the the value of i after the loop, for the binding of i in any function generated inside the loop (example: print could actually talking to an IO object that defers output by prepending lambdas to a chain - I've seen web containers deferring view rendering this way), for name masking, and for the consequences of any flow-control/continuation/exception mechanism that may cause an early loop exit.


I'm unsure what the difference is between the first two…are they not equivalent? The lexical scoping could be interpreted differently, sure (allowing your third example the benefit of the doubt) but this is where the "Swift is consistent" comes into play. You already know Swift's scoping rules, it's one of the first things you learn, so using that knowledge you can figure out this code as well.


No, they're not equivalent. See, that requires making an assumption that I didn't about lexical nesting. They merely do the same thing in this example.

I do not know Swift's scoping rules and having read through the introductory guide and the flow control chapter I still can't tell you if the loop variable remains visible after the loop, and what it is bound to if so.

No doubt that if I sat down with a REPL or an IDE it'd hopefully be evident by example within a few seconds, but the claim was about dry-reading, not interactive experience.

I'll leave it there with an admonishment against making assumptions/declarations about the obvious-ness of something you're already familiar with.


In every language I’ve used with a for-in construct (or equivalent), the iterated variable is scoped to the loop. That seems to be implied by the syntax, too; it’s equivalent to “for all i in [1,9] where i mod 2 = 0”. If you saw that in a math context, you’d expect i to have no meaning beyond the iteration.

Is there a case to be made for other behavior? Or an example of a language that intentionally treats it any differently? (Unless you go out of your way to iterate over a variable you’ve declared in an outer scope in C, but then that’s not really a for-in construct.)


Python and JavaScript are languages that do not behave the way you mentioned.


JS works that way with plain for loops, but not for-in or for-of. The following fails on the last line, for example:

  for (const i in [1,2,3,4]) {
    alert(i);
  }
  alert("i is now " + i); // ReferenceError
(Not that JS is exactly known for consistent and predictable behavior on edge cases, of course...)

Python I don’t have experience with, and it appears I stand corrected on that front! I wonder how intentional it is, and what sort of use case it enables (and if it’s considered good practice to use).


Python does this mostly because it doesn't have block scope, only function scope like JS's `var`. After using Python for a decade, I think this is mostly a mistake. I would configure (or edit) my linter to prevent me from using it if I needed to write a lot more Python.


Lexical nesting? While I agree with the statement you’re making — that reading code requires you to understand more about what the code means, in that context/runtime, I also think the example you’re picking apart reminds me of the dangers of undefined behaviour in C++. If the language specification plus its test cases still has implementation-specific details, then while by all means measure or discover them, but I would both say this is not unique to Swift and is just part of the complexity that is not writing CPU instructions directly that target only one type of CPU design. Maybe I’m taking this too far, but my point I suppose is that I know what was intended by the swift code even if the exact details are optimized out from underneath me by a compiler or language library. Might as well argue that you should have full unit and integration tests for every target platform to ensure the code behaves the way you expect, and even then you’ll still hit platform-specific edge cases, particularly on the wide variety of platforms Linux supports... I do tend to trust Clang, but even it can vary greatly from (hopefully major) version to version.


Yes, I'm definitely trying to point out the distinction between reading code and understanding its intention vs understanding implementation mechanics and awareness of the possible alternative behaviours. It's easy to point out that C++ is amongst the worst offenders in that regard, but even a language with a reputation for "programmer happiness" like Ruby (which I absolutely adore) is full of magic, almost every nontrivial expression glossing over a huge pile of conceptual and mechanical object-functional devices that beginners trip over quite routinely (don't even get me started on the thick layer of sorcery that Rails slathers over the top of that).

So I'm not actually calling Swift a major offender, but noting generally that I don't there's ever been a language in which the intuition gap between intention and mechanics was small enough to be irrelevant.

(something at the back of my mind is now whispering "Scheme", but too quietly to be taken seriously)


A where clause on a loop is nice, but I think it's likely it will either be abused or be limiting. I've always favored systems that work well together to build something more than the sum of the parts. Perl is an example of that in many cases.

  for my $i ( 1..(10-1) ) {
    next if $i % 2 == 0;
    print $i;
  }
Here Perl's post-conditional syntax (which works only on a statement and not a block so it's more manageable) combines well with "next" (Perl's version of "continue") to very clearly convey intent without a lot of extra boilerplate. An additional clause is often just an additional line. Comments after each can provide additional context as needed. I think this is a more flexible way to accomplish the same thing, and more readable in all but the simplest of cases. And for those simplest of cases, I would use a grep, which is also useful in other parts of the language:

  for my $i ( grep { not $_ % 2 } 1..(10-1) ) {
    print $i;
  }
Any syntax learned that works as a special case to only a single structure is either a case of missed opportunity or a case of extra syntax that needs to be learned which shouldn't be, IMO.


That where is not a one-off construct, it's used with switch statements:

  switch i {
  case 1...100 where i % 2 == 0:
      print("\(i) is a small even number")
  case ..<0:
      print("\(i) is negative")
  default:
      print("I couldn't care less")
  }
Or generic constraints:

  struct Vector<T> where T: Numeric {
      // ...
  }
If you so wish, you can always use any of the functional constructs as well, as in

  for i in (0..<10).filter { $0 % 2 == 0 } {
      print(i)
  }


So it looks like it's a general modifier to a range type, so can be used where those work. That's slightly better, but if filter exists, why not just use that?

One of the biggest criticisms that Perl gets is that there's so many different ways to do things, and that's somewhat deserved, so the question is what does "where" offer that "filter" doesn't other than another keyword and concept you have to learn to understand the language? What's the point if it's not really that much more expressive or clear?

> struct Vector<T> where T: Numeric

Is this really the same thing? This seems like a case where the same word is used to mean something else, even if they are loosely conceptually the same.


It isn't actually a modifier to a range type -- it modifies other constructs, like looping or conditionals. The examples don't show it but literally any condition can go in the where clause, even something like `where random() % 7 == 2`.


Better learn a new, generic concept than obscure syntax I’ll see combined 1000 different ways on the same codebase.


I find this style (Rust)) more readable end easier to extend:

RangeInclusive::new(1, 10) .filter(|x| x % 2 == 0) .for_each(|x|{ println!("{}", x); });

(though you could write it the same way, for i in 1..=-10 { if .. { .. }}


The same can be written in Swift as

(1...10).filter { $0.isMultiple(of: 2) }.forEach { print($0) }




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

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

Search: