Making `for` simpler and more regular

When I learn my first programming language, which is C, the most intuitive language feature to me is assignment. I found it’s difficult to imagine how an equation could be changed during time?

After I accept the concept of mutable variable binding, rest of the language features like for or while become reasonable.

What if I did not start from learning assignment?

So as not to mislead anybody: I spend all day coding in it for clients, with three-day breaks here and there to do Scala training for companies that need to get engineers up to speed. (Nowadays I’m a consultant, working for Bill Venners at Artima.)

Ok. I guess in my mind it didn’t sound the way you interpreted it, sorry about that :slight_smile:

Your post comes across a bit as “feigned surprise”

I tried to convene that I’d like to understand in what way people get to know and learn the language that makes this particular corner prone to confusion, because your experience is different to mine and I can’t tell how that went unless you tell me.

Regarding mental hiccups and writing bugs, they are going to continue to happen no matter how you alter the language. Mental hiccups are just that, I would not alter the language optimizing for mental hiccups, and writing bugs is something the language can attempt to ease but how do you make sure you are not unfolding a different kind of new confusions. We could list specific functionality in many languages destined to addressed specific kinds of bugs and those still happening (memory management in rust, null safety in kotlin, etc).

If the point is “behavior isn’t consistent with destructing in another context” then why only the pattern matching on val context is mentioned? Pattern matching exists in other places, which one is more relevant? why make it more like val, and not more like partial functions and collect like operations? Akka is pretty big in scala land, and recieve is such a partial function.

This is why I find it important to understand how you learned the language, and why some concepts stuck more than others in your head, to understand which contexts are more surprising than others. My case, for instance, is that I learned for as syntactic sugar for foreach, map, flatMap, and filter, back then I did not know about monads and the like nor I grasped the full extent to which it could be used, but I quickly mapped for comprehension to those 4 operations and I really liked the filtering capability.

So as not to mislead anybody: I spend all day coding in it for clients, with three-day breaks here and there to do Scala training for companies that need to get engineers up to speed. (Nowadays I’m a consultant, working for Bill Venners at Artima.)

Sort of same here.

For the record, I spend a tiny portion of some days trying to catch bugs in the library and/or give advice about library performance. I’ve never worked on the compiler itself. That falls to cleverer people.

I do spend ~50% of my time working in Scala at work, though, so I’m not completely inept.

Oops, that means I have the wrong mental map of your user name to your real name. Doh!

Hey, I am sure this already has been brought up, but I think for is trying to serve too many masters. There is collection heavy code with filtering and such and then there is monadic code. And while there exist a big overlap between the two, maybe it is still worthwile considering the two in isolation.

I, for myself, am writing a lot of monadic code recently and noticed that for is really getting into my way in doing so. Here is a snippet that illustrates some of the pain-points:

for {
  cur <- pos.value
  cacheKey = (key, cur)
  cache <- resultCache.value
  res <- if (cache isDefinedAt cacheKey) (for {
      (results: A, newPos) <- pure(cache(cacheKey))
      _ <- pos.update(newPos)
    } yield results) else { ... }
} yield res

With some form of do-notation this could be written as

do {
  val cur <- pos.value
  val cacheKey = (key, cur)
  val cache <- resultCache.value
  if (cache isDefinedAt cacheKey) do {
    val (results: A, newPos) = cache(cacheKey)
    pos.update(newPos)
    pure(results)
  } else { ... }
} 

I am purposefully using do here to clearly distinguish from the existing for.

Note that val ID <- EXPR translates similar to ID <- EXPR in for and val ID = EXPR translates similar to ID = EXPR in for. To be more compositional, the last value in the do-block has to be monadic, hence the pure(results) in the translation to explicitly lift results. At the same time

[[EXPR1; EXPR2]] translates to val _ <- [[EXPR1]]; [[EXPR2]].

That is, sequencing is monadic sequencing. This makes “builtin side effects” more cumbersome to use via val _ = println(...). However, I feel this overhead is appropriate since do switches citizenship between builtin side effects and managed side effects.

I am aware of monadless and other approaches to ease monadic programming, but I couldn’t find a dotty SIP.

@odersky You mentioned, you don’t want to go back to vals. Would that also hold if one separates for and an alternative do-notation? I am purposefully using vals here to highlight that do blocks “just” overload sequential composition.

edit: Thinking about it a bit more: If we ignore overloading of ; for a moment – only having val f <- EXPR to translate to flatMap which scopes over the rest of the current block would already go pretty far.

1 Like

If anyone is interested in playing around with it: I quickly draft implemented the val a <- ... syntax within normal blocks (not for-comprehensions) here https://github.com/b-studios/dotty/pull/2

1 Like

Sometimes I want to do logging inside a for expression, but the syntax for doing so is annoying. This complaint applies any method call where you want to throw away the result.

for {
...
  _ = logger.info(...)
...
}

I’d prefer,

for {
...
  logger.info(...)
...
}
1 Like

so be it with another one

for { 
   case if Left((x, "salmon")) <- myEither
 } yield x + x`

I think you mean something like

for {
...
_ <- Option(logger.info(...))
...
}

But I agree, have had to do this also. For the Option wrapped version, I usually just create a method like def localLogger(str: String): Option[Unit] and do _ <- localLogger(...).

What bothers me about for is the resulting visual clutter if you want to immediately reuse the yielded value.

(for {
  result0 <- fromService(...)
  result1 <- anotherCall(result0)
} yield result1).fold(..., ...)

The extra wrapping in a parenthesis just makes things look very messy.

1 Like

The exact same thing happens with if.

(if (condition) methodThatMakeCollection1() else methodThatMakesCollection2()).fold(...)

You might do this more with for, but the effect is the same, and it seems to me that any “fix” would at best not be worth the headaches and at worst would actually make the language syntax more complex while producing no significant benefits.

I use the following for that:

@inline def ife[A](b: Boolean, vTrue: => A, vFalse: => A): A = if (b) vTrue else vFalse
1 Like

There is a canonical issue for the narrower question of what signals a filter in a generator, and I think the workaround syntax suffices:

for (x: X <- e)

aligns with

val x: X = e

but anything else is taken as a pattern. To induce filtering:

for (x @ (_: X) <- e)

An incremental improvement would be to align midstream assignment.

There was a previous PR to warn on “postfix if” following midstream assignment, because of the expectation established by perl syntax.

I think it’s in most cases it’s a bad idea to immediately reuse the yielded value. Name every intermediate result! Multi-line code without local definitions is a code smell. I see too much Scala code that ignores this principle, at the price of legibility.

3 Likes

It may be not obvious. But when I am debugging such code, this is very annoying:

  • to toggle breakpoint
  • to watch intermediate results

I just mentioned this to my java friend who likes to compose a long vertically aligned expression as though it were a virtue. (He had a type inference problem.)

I wish the psychologists would explain why we are so quick to acquire certain idioms which then prove iron-clad. That is, they become mental shackles.

Beautiful words. I have just remembered:
“A nuclear blaster is a good weapon, but it can point both ways”
The Foundation series Isaac Asimov

I’m (kinda) resurrecting this thread here, sorry, but I found this idea appealing. If we look at ES2015’s async/await syntax for a moment, and think about it in a Scala-ish way, then we could come up with something like (to adapt Generating Pythagorean triples in Scala - #14 by yawaramin - Question - Scala Users ):

def pythagoreanTriples = {
  for c = Iterator.from(1) // for does the job of 'await'
  for b = 1 until c
  for a = 1 until b if c * c == a * a + b * b
  val triple = (a, b, c) // Just to show a normal assignment

  yield triple // yield does its usual job
}

True, I don’t have an analog for ‘async’, because I don’t feel that Scala needs it.

Overall, this makes it slightly more verbose (I think?) but in exchange also ‘flatter’.

I personally don’t care for this – IMO, it doesn’t express scopes as clearly as the existing syntax. (OTOH, I don’t much like the async library, either, so take this opinion for what it’s worth.)