Suggestion: if-comprehensions

Does the for-ification also work with more complex if-constructs like if (a <- aOpt || b <- bOpt)?

I guess that would need to be transformed to something like for(c <- aOpt.orElse(bOpt))?

I’m not clear on if it would work in the general case - how would eg if(a <- aOpt || foo || b <- bOpt) be handled?

1 Like

Maybe you tell us how this is supposed to work?

if (a <- aOpt || foo || b <- bOpt) { ...um...what goes here? }

I don’t see what the use is in naming a or b because you can’t know which one you get (and they might not be the same type). So you can’t actually use them.

At that point, you don’t gain anything over

if (aOpt.isDefined || foo || bOpt.isDefined) { ... }

do you?

So I don’t think || makes sense in a comprehension.

If either a or b is acceptable, you can

if (x <- aOpt.orElse(bOpt)) foo(x)

That is an excellent point.

Thoughts on if(a <- aOpt && !b <- bOpt)?

We should probably either not allow it, or make b available in the else branch.

You can’t make b available in the else branch because the reason you are in there can be that aOpt.isDefined returned false

This is one of the reasons that I think the parrallel with booleans doesn’t generalize well

1 Like

The only problem with this approach is that it elevates Option from a standard library element to a syntactical element of the language.

The same is true for flatMap, withFilter, map, and foreach, but they are much more widely defined than getOrElse. get isn’t acceptable for use, as it returns T in some interfaces and Option[T] in others. getOrElse is probably the defined target method here. Either, Try, and Option all have that. The Map interfaces have that as well, but they require a key as well as a default.

Are we planning on issuing the transformation for any type that contains a getOrElse method? Does the type also need flatMap, and map as well?

I’m ok with targeting getOrElse for syntactic sugar as long as it would apply to more than just (pun intended) Option.

Can’t this be built—if at all—on top of match case? IIUC it is intended as a slightly shorter syntax for a (potentially nested) match expression. So just expand it to the longer match expression. And then you only need the methods which already have a special status because of name based matching.

1 Like

Foo for thought: in the language we’re building, one is going to be are able to write:

if aOpt is Some(a) and bOpt is Some(b) and a.foo() == b.bar() then 
   // Use a & b...
else ...

Some other cool examples:

fun add(x, y) =
  if x is Some(xv) and y is Some(yv)
  then Some(xv + yv)
  else None

fun nonZero(list) =
  list is Nil or
    list is x :: xs and x != 0 and nonZero(xs)

fun findFirst(list, p) =
  if list is
    Nil then None
    x :: xs and
      p(x) then Some(x)
      else findFirst(xs, p)

fun zipWith(f, xs, ys) =
  if xs is Cons(x, xs)
    and ys is Cons(y, ys)
    and zipWith(f, xs, ys) is Some(tail)
    then Some(Cons(f(x, y), tail))
  else if xs is Nil
    and ys is Nil
    then Some(Nil)
  else None

fun mapPartition(f, xs) = if xs is
  Nil then (Nil, Nil)
  x :: xs and mapPartition(f, xs) is (l, r) and f(x) is
    Left(v) then (v :: l, r)
    Right(v) then (l, v :: r)

if x <=
  31 then "invisible"
  57 and x >= 48 then "digit"
  90 and x >= 65 then "uppercase"
  122 and x >= 97 then "lowercase"
else "symbol"
4 Likes

No, because if you have a sum type you don’t know which branch to match on. Also, you have to finish your typing pass first so you even know what the branches are.

If it’s methods, you can do a purely textual rewrite, and then if the methods aren’t available, give an error message. (Did you know, for example, that you can–at least in Scala 3 where I checked it–write flatMap and withFilter and so on as extension methods and it still works?)

You would need getOrElse to support if p then a else b, and you would need orElse to support if p then a else if q then b else c.

1 Like

I would disallow it. You would write that logic as if (a <- aOpt && b.isEmpty).

You could if you have to specify the pattern like in @LPTK’s language:

Then you are also no longer limited to data structures that are isomorphic to Option.

1 Like

Wouldn’t that be closer to flow typing that it would something like a for-comprehension?

I think the magical behavior of conditions in ifs sounds atrociously difficult to understand.

About the only thing I can imagine making sense here would be to add a for comprehension piece for “else”, although I think even then the specific semantics would need some working out:

for a <- aOpt; b <- bOpt; if c yield foo(a, b) else d

For Option this would be more or less the same as:

(for a <- aOpt; b <- bOpt; if c yield foo(a, b)) orElse d

For Seq it would be more or less the same as:

(for a <- aSeq; b <- bSeq; if c yield foo(a, b)) ++ d

Or in either case it could be something like:

{
   val tmp = (for a <- aThing; b <- bThing; c yield foo(a, b))
   if ! tmp.isEmpty then tmp else d
}

(Although that one feels somehow unsatisfying to me, despite the problem that the plus in Option is orElse and the plus in Seq is ++. ++ on Option forces it to become an Iterable instead of remaining an Option.)

3 Likes

I don’t think this works for Seq, as it unconditionally appends d

Ahh. Good point. I was fixating on the “plus” part and not the “only when zero” part. Whoops. :sweat_smile: And “else” definitely implies that it only happens if the prior result was zero.

Seq doesn’t really have an operation like that. “orElse” on Seq is the orElse of PartialFunction, and not at all like orElse on Option. So if x.isEmpty then d else x is about the best you can do without adding something new.

Which is perhaps another illustration of how this isn’t really a very universal concept, and perhaps when it’s wanted it ought to be done in a way specific to the type involved.

2 Likes

I kinda agree, but to be fair, I find for difficult to understand too :sweat_smile:

I basically never use for any longer. Too much sugar in my diet is bad.

2 Likes

Same here. I found for-yield mystifying when I started with Scala. Then, once I understood what it does I loved it for a while (especially on futures). Now, I tend to write map/flatMap/filter explicitly.

2 Likes

Agreed. That still gives us signatures in Option, Try, and Either in the standard library and would allow users to create interfaces that get desugared as well. Required methods are getOrElse and orElse.

Not any closer to flow typing than regular match expressions.
IMHO for-comprehensions are not the right feature to base this on either way.

1 Like

another language that do flow analysis is java with JEP 394: Pattern Matching for instanceof (looks like nobody mentioned it in this thread yet)

jep snippet:

Rather than using a coarse approximation for the scope of pattern variables, pattern variables instead use the concept of flow scoping . A pattern variable is only in scope where the compiler can deduce that the pattern has definitely matched and the variable will have been assigned a value. This analysis is flow sensitive and works in a similar way to existing flow analyses such as definite assignment.

terminology is rather unfamiliar to me, so i won’t comment on it, but my point is that java is yet another language that already does some analysis of type or value refinements. since java is mainstream, it made flow analysis mainstream too already, so maybe it’s not that scary. note that java doesn’t do flow-typing as described in wikipedia, because it needs additional identifier that receives refinement. on the other hand that makes it more similar to the proposal in this thread.