ann-arcana

Queen of Burgers 🍔

Writer, game designer, engineer, bisexual tranthing, FFXIV addict

OC: Anna Verde - Primal/Excalibur, Empyreum W12 P14

Mare: E6M76HDMVU
. . .



JoshJers
@JoshJers

The question here is "what's your favorite non-C-like flow control statement/construct that you've seen in a programming language" but I'm gonna start with a little preamble.


I'm very familiar with C-style flow control:

if (condition)
  { thing; }
else 
  { otherThing; }


// And, thanks to C++17:
if (auto v= initializer(); condition) 
  { thing(v); }
else 
  { otherThing(v); }
for (int i = 0; i < 5; i++)
  { thing(); }
for (var e : elements)
  { thing(e); }
while (condition)
  { thing(); }

(plus, ya know, do/while and goto)

I've seen a few others, like Rust's loop (basically a while(true)):

loop
{ 
  thing(); 
  if (something) 
    { break; }
}

and Swift's ranges, which are nice:

for i in 0..<4
  { thing(i); }

What I'm wondering is: what are your favorite bits of flow control logic (loops, jumps, conditionals, etc) that don't show up in C or C++? Are there loop forms that you think are truly elegant? Is there a switch-like construct that doesn't suck like C's switch statement does?


ann-arcana
@ann-arcana

So I have a whole talk about this, which I could link but I feel weird about linking it because it's pre-transition and an old version, so this is kind of a Cliff Notes version of that.

Heresy is a Lisp-1 dialect I created inspired by old-school BASIC, but designed originally to teach functional programming to people who'd only ever worked in old-school imperative languages, but later mostly to teach them to myself by implementing them.

As a pure functional language, this means there is no mutability in Heresy whatsoever, and only limited side effects (just file and console, basically). But you can't write BASIC without FOR loops right? And while you can write a functional FOR loop, it's actually not a control structure that's often useful except for the side effects it generates, otherwise you'd just use a map or filter.

So we need another solution, and that solution is recursion, and escape continuations.


Here is the original macro (it has since expanded in complexity considerably, and I'm leaving some stuff off screen here):

(define-syntax-rule (for-loop var lst x body ...)
  (let/ec break-k
    (syntax-parameterize 
        ((break (syntax-rules () 
                  [(_ ret) (break-k ret)]
                  [(_) (break-k)])))
      (let loop ((cry-v x)
                 (l lst))
        (syntax-parameterize
            ([cry (make-rename-transformer #'cry-v)])
          (cond [(null? l) cry-v]
                [else (let ([var (car l)])
                        (loop
                         (call/ec
                          (lambda (k)
                            (syntax-parameterize
                                ([carry (make-rename-transformer #'k)])
                              body ...)
                            cry-v))
                         (cdr l)))]))))))

What we have there is a nested set of escape continuations that allow us to "break" out of the loop, as well as control when we jump to the next iteration of the loop. The loop itself is a recursive let that calls itself while carrying forward both the remaining chunk of the list and a "carry" value that we can use to build up some value to return either at the end of the loop, or explicitly through a break.

OK, that's a lot of words though, some of which I even have trouble remembering how they work, so here's a simple example of what it looks like in use:

(def fn fact (n)
  (for (x in (range n to 1 step -1) with 1)
    (carry (* cry x))))

This should look a lot like a for loop in BASIC but for the parens, and some weird keywords. You got your for, there's an extra range in there, but then what's the rest of it?

For starters, range is actually just a macro that adds some syntax sugar over a function that generates lists of numbers. You can actually put any list value in there, or a function call that returns a list. In this case though, we're just telling it to generate a list that starts at n and counts backward to 1.

Where the fun bit is, is with. with lets us define the starting value of cry. cry is essentially a keyword acting as a variable, and refers to an internal var we define within the loop, that initially contains the value of the with clause.

Within the body of the loop, we can call carry, and pass it a value, and it will start the next iteration of the loop with that value contained in cry.

The result is incredibly flexible, since for can iterate over anything that is a list, and with can be any value, you can do all kinds of iterative calculations in fiendish and inventive ways that Heresy's parent, Racket, needs dozens of different macros for.

And thanks to Racket's hygienic macros, we can safely nest our loops. Here we build a deck of cards with a pair of for loops (even using list ops to build the initial list for the card values). We can forgo with here because the default value of cry if not set otherwise is just the empty list.

(def cards
  (for (suit in '(♠ ♣ ♥ ♦))
    (carry (append (for (x in (append (range 2 to 10) '(J Q K A)))
                     (carry (join `(,x ,suit) cry)))
                   cry))))

But of course, we need not stop there. The flexibility allows us to essentially allow us to invent new kinds of control flow of a kind. Here's essentially a variadic version of the F# "forward pipe" operator, as a simple function with a for loop.

(def fn :> (initial-value . fns)
  (for (f in fns with initial-value)
    (carry (f cry))))

We just walk over a list of functions and apply each to cry in order. So we can do things like this:

(:> 5
    inc
    (partial - 5)
    even?)

And we needn't stop there! With a couple little macros, next we can build the Clojure threading operator:

(def macro f> (f args ...)
  (fn (x)
      (f x args ...)))

(def macro -> (iv (f args ...) ...)
  (:> iv
      (f> f args ...)
      ...))

And now we can do this, just like Clojure, only underneath it all is just a for loop and some recursion!

(-> '(1 2 3 4)
    (left 2)
    (append '(a b)))

;=> '(1 2 a b)

But we can keep going! Remember, we can set any initial value, so ... why not a namespace. Heresy has a functional object system called Things, which are their own whole ramble for another time (they're actually lambda closures underneath!), but the short version is they work kinda like JavaScript objects in that they can kinda act like an object and kinda act like a map, so they're perfect for our purposes.

We just need to do a little set up here. First we start with an empty object:

(describe State)

And we build up some macro operations. First we need to be able to set a var:

(def macroset :=
  [(:= (name ...) var value)
   (fn (s) (thing extends s
                  (var (let ([name (s (quote name))] ...)
                         value))))]
  [(:= var value)
   (fn (s) (thing extends s (var value)))])

This one lets us just do a thing and ignore the result:

(def macroset :_ 
  [(:_ (name ...) f args ...)
   (fn (s)
       (let ([name (s (quote name))] ...)
         (f args ...)
         s))]
  [(:_ f args ...)
   (fn (s)
       (f args ...)
       s)])

And return them ...

(def macro return (name)
  (fn (s) (s (quote name))))

And finally the little rug that ties the room together:

(def fn do> fns
  (apply :> (join State fns)))

So what does this do?

Do!

(do>
 (:= x 5)
 (:_ (x) print (format$ "Value was #_" x))
 (:= (x) x (+ x 5))
 (:_ (x) print (format$ "But now it's #_" x))
 (:= x "Behold, a monad ... -ish.")
 (return x))

;=> Value was 5
;=> But now it's 10
;=> Behold, a monad ... -ish.

Yeah we just accidentally invented Haskell's do notation in the most back-ass-ward way possible.

And underneath it all, the humble for loop.

This is the trick that made my career.


You must log in to comment.

in reply to @JoshJers's post:

OCaml's try-with expressions for exception handling are quite nice, mostly because they are, well, expressions and not statements. This means you can return a value from a try-with expression and even put it inside a different expression. Unlike, say, Java's try-catch, this means that handling exceptions does not need to completely mess up the flow of your function!

It's also a nice bonus that you can use these as exception patterns in a regular match expression like this

let value = match arr.(42) with
    | exception Not_Found -> "nope" 
    | 0 -> "zero"
    | x -> string_of_int x

This can be much more convenient sometimes

The Common Lisp equivalent of a for loop, DO, lets you define multiple loop variables with init/step forms instead of just 1 like in C. As a result, it's somewhat common to have empty loop bodies and the whole loop logic is in the variable update.

Fun fact about Rust loops: they are expressions and can return a value. If you use break with an argument, that's the return value.

i like to eliminate explicit loops by using map or each in janet-lang which applies a function over a provided sequence: (map collects results in an array, each is mostly used for evoking side-effects)

(map inc [1 2 3 4 5]) => @[2 3 4 5 6]

(each inc [1 2 3 4 5]) => nil

because the iteration isnt really the important bit here, it's the operation applied that matters...

oh shoot, forgot cond and case:

cond lets you evaluate an expression, its like a list of else-ifs and case is basically a switch except with the usual dynamic typing going on

https://janetdocs.com/cond

https://janetdocs.com/case

Two suggestions:

  1. Is it possible in your language to reduce the number of control loop keywords? E.g., I think that what go does with for to handle all the looping is kind of cute.
  2. I find python for...else and while...else statements more useful than I thought I would when I first encountered them. This is just a loop with an else tagged onto it: the else happens if the loop exits normally by its condition becoming false, and doesn't happen if the loop exits because of a break or return. Incredibly useful when you're writing a loop to find something, want to break when you find it, and want to run some code when you didn't find what you were looking for.

And another related idea from go that rust also has that isn't really new control-flow constructs: ditch the parentheses around the condition, but require the braces around the loop body.

I'm of two minds of reducing the number of loop keywords: on one hand, less ways of doing things is usually better (see C/go for loops which can be constructed to be basically anything), but on the other hand there's something to be said for having a few extra keywords/constructs to more clearly define your intent ("while" loops have a very specific purpose vs. Rust's "loop" which is more freeform, vs "for" or "foreach" which generally are used to iterate over ranges).

I think the right answer is "use exactly as many as needed to make it easy and clear to describe the intent without the possibility of a typo or missed statement turning into a totally different style of loop" - whatever that ends up being lol

for...else is neat (not having to have a "wasFound" variable or equivalent is nice), although I definitely would have picked a different way to write it than "else" because if you gave me 2 guesses as to what for...else does I would have guessed two completely different things - but ignoring the syntax, it's definitely useful!

And re: ditch parens/require braces, that's exactly what I've been doing - so it's good to know that there are a couple of real, good languages that made the same choice!

The way I think of the "else" bit in for...else is that in your loop you're going to guard the break or return statements with some sort of conditional, so you have a loop like this:

for x in thingy:
    if is_acceptable(x):
        print(f"Found {x}")
        break
else:
    print("Didn't find it")

So implicitly, the "if" statements in the loop after the first iteration are a little bit like "elif" statements, since you only consider an "if" statement the second time through the loop if you didn't break out of the loop in the first iteration.

And the else that's after the for is like an "ultimate else" that's the else after all those "if" statements.

Was going to say the same. We use loops in C for more than we should. Comprehensions in Python are fantastic. I think it's very hard to add these to C++ because there's very little agreement on what a range / list / set is though. So I think we're out of luck on seeing it before C++50 ... :(

Of all the languages I have used, Ruby is the one that seems to have thought in most depth about usability for programmers. It's the only one where I think "ideally I'd like to be able to do... this" and that thing works. The other great loop variant it offers is do ..... while ( condition ) for those situations when you know you want to run the code at least once and maybe loop.

Yeah I'll admit ruby's |variable| thing is bizarre to me (I don't love the disconnect between the loop variable and the thing generating the variables) but being able to just say "do a thing 5 times" is really nice - it's a very clear way to express intent, which is crucial for code readability/maintainability

Once upon a time I read an old Scheme greybeard say words to the effect that if a language does not have cond, it is incomplete, and while normally I'd balk at such absolutism ... every time I have to use a language without cond, I feel like he might've been right.

Labelled breaks. Otherwise known as "goto that doesn't trigger people". It's a goto where the target can only be the end of a scope you're already inside, e.g.

for (...)
{
  for (...)
  {
    while (...)
    {
      if (...)
      {
        break we_outta_here;
      }  
    }
  }
  we_outta_here:
}

Yeah I think Rust has labeled loops, which is neat.

goto is great, I use it more than most programmers think I probably should (not frequently but more than 0), but there's definitely something to be said for "a goto that is limited to only allow a specific kind of movement" which, now that I think about it, is all any code flow construct is anyway.

One I've used a lot is Elixir's with operation

with pattern_match <- operation do
  # ...ops
else
  negative_match_1 -> 
    # ...other ops
  negative_match_2 ->
    # ...other other ops
end

If the positive condition matches a pattern (or patterns, comma separated in the with section) from the output of a function or pattern match, then run in the do block, otherwise, try and match on anything that exists in the else block and run code there.

Surprised that nobody has mentioned call-with-current-continuation in Scheme yet. It’s the mother of all control flow structures. You can use it to implement exceptions, early return, suspendible coroutines… it’s one of the reasons Scheme is so delightfully minimal (perhaps even pathologically minimal). Haskell also provides them, as they fall out fairly easily when you have lazy semantics, but the docs warn that “abuse of continuations can produce code that is impossible to understand or maintain,” which is in my experience totally valid.

My personal favorite is a concatenative-style while. It comes in many flavors, but the important part is that unlike C-style while () {} or do {} while () you can have logic both before and after the predicate check. This is similar in spirit to break, but still only allows a single breakpoint in the loop which helps visibility, and actually allows rewriting many break usecases without the problematic keyword. No matter if its Factor's [ pred ] [ body ] while or Forth's BEGIN pred WHILE body REPEAT or even Quackery's [ pred while body again ].

TIL'd about catch when in C# and think it's very nice! Snippet from docs:

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        Console.WriteLine(MakeRequest().Result);
    }

    public static async Task<string> MakeRequest()
    {
        var client = new HttpClient();
        var streamTask = client.GetStringAsync("https://localHost:10000");
        try
        {
            var responseText = await streamTask;
            return responseText;
        }
        catch (HttpRequestException e) when (e.Message.Contains("301"))
        {
            return "Site Moved";
        }
        catch (HttpRequestException e) when (e.Message.Contains("404"))
        {
            return "Page Not Found";
        }
        catch (HttpRequestException e)
        {
            return e.Message;
        }
    }
}

Prolly not my favourite though. That'd be the else statement on Python loops that others have mentioned.

Oh that's cool I actually didn't know C# had that! I've only recently picked it back up and it's changed a ton over the last ... well, a while. Last time I used it I think I was working in C#3 and now it's on 11! (if you haven't checked out the raw string literals that they just added, those are also super nice and solve a lot of problems I've had with other languages' implementations of them)

It's cool to see them continuously improving the language! Although at the same time I get the feeling that all languages are gradually converging 😂

Unfortunately I'm limited to C# 9 because that's what the Unity game engine uses. I tend to avert my eyes from the newer features so I don't get too sad 😛