Proposal: result-forwarding ternary operator

# Michael Rosefield (7 years ago)

(I've also put this on reddit www.reddit.com/r/javascript/comments/7129tn/proposal_resultforwarding_ternary_operator,

which I've copied this from. Hope the formatting doesn't go haywire...)

First course of action for this proposal is, obviously, to come up with a better name for it....

Motivation

As with the 'optional chaining' proposal for tc39 tc39/proposal-optional-chaining, this operator is a

way to avoid excess and annoying code from safety-checking.

The optional chaining proposal, above, follows a chain and short-circuits it upon acting on a null object, returning a safe 'undefined' result; it can be thought of as an extended 'if' sequence. It looks like this:

// safeVal = result of someProp, or undefined if looking for props on null obj const safeVal = blah?.someMethod()?.someProp;

This proposal provides for an 'else' scenario, particularly in situations where chaining isn't appropriate, by forwarding the result of a truthy conditional check to a single-parameter function.

Syntax

condition ?! fn : expr

Parameters

  • condition: any condition, identical to use in standard ternary
  • fn: function taking single parameter, which is the result of evaluating condition
  • expr: any expression, identical to use in standard ternary

Usage Example

// temporary variable const temp = getSomething(), foo = temp ? doSomething(temp) : doSomethingElse();

// repeated code, possible side-effects const foo2 = getSomething() ? doSomething(getSomething()) : doSomethingElse();

// proposal, no chaining const newFoo = getSomething() ?! x => doSomething(x) : doSomethingElse();

// proposal, chaining const newFoo = getSomething() ?! x => { doSomethingFirst(); return x.doSomething(); } : doSomethingElse();

Notes

The choice of '?!' is entirely arbitrary and not a core part of the proposal.

The result of the conditional check is not passed on to the falsey path, because it seems pointless to do so.

# Isiah Meadows (7 years ago)

Few issues:

  1. This is already technically valid code: cond?!fn:orElse is equivalent to cond ? !fn : orElse
  2. Have you considered do expressions (stage 1 proposal)? They work a lot like IIFEs, but allow easy definition of computed constants.
  3. Have you considered using in-condition assignment or just factoring out the computed condition into a separate variable? Sometimes, a little verbosity helps.

Using do expressions, your second code sample would look like this:

const newFoo = do {
    let x = getSomething();
    if (x) {
        doSomethingFirst();
        x.doSomething();
    } else {
        doSomethingElse();
    }
};
# Sebastian Malton (7 years ago)

An HTML attachment was scrubbed... URL: esdiscuss/attachments/20170919/9276bf15/attachment

# Andrea Giammarchi (7 years ago)

I don't think do is "much longer" than your last example, however, it can be shorter

const newFoo = do {
  let x = getSomething();
  x ?
    (doSomethingFirst(), x.doSomething()) :
    doSomethingElse();
};
# Michael Rosefield (7 years ago)

We still have to explicitly create a variable (x), either in the do block or before that ternary, and the bracket-enclosed comma-separated expressions are... not to my taste.

This was always about syntactic sugar and concision, as there are always other ways to go about it; as I commented in my reddit post, both operators can be done functionally:

const $equivFn = (cond, ifTruthy, otherwise) => cond ? ifTruthy(cond) : otherwise(), foo = $equivFn(getSomething(), x => doSomething(x), () => doSomething()), equivFoo = getSomething() ?! x => doSomething(x) : doSomethingElse();

// normal ternary const $ternary = (cond, ifTruthy, otherwise) => cond ? ifTruthy() : otherwise(), foo = $ternary(checkSomething(), () => doSomething(), () =>

doSomething()), equivFoo = checkSomething() ? doSomething() : doSomethingElse();

... but it's not elegant.

And I appreciate ?! was a bad choice, but can easily be substituted by anything else.

# Isiah Meadows (7 years ago)

I'll just note that the only two languages I know of with a feature like this is Haskell with its maybe fn orElse m* function in Data.Maybe and Scala's Option[T].mapOrElse(fn, orElse). Here's what many other languages do:

Several languages use some form of if let, including Rust, Scala, and Swift:

// In Swift
let newFoo
if let x = getSomething() {
    doSomethingFirst()
    newFoo = x.doSomething()
} else {
    newFoo = doSomethingElse()
}

Clojure offers the macro (if-let), which does mostly the same thing (Common Lisp has a similar macro):

;; Top-level declaration
(def new-foo
  (if-let x (get-something)
    (do
      (do-something-first)
      (do-something x))
    :else (do-something-else)))

OCaml uses pattern matching, and C/C++, most of its derivatives (like Python), and Kotlin just do if (x != NULL) ... else ... or similar. (Kotlin has flow-sensitive typing like TypeScript, which helps avoid mistakes.)

  • I might have gotten the argument order wrong - I'm not a regular Haskell user, and it's not commonly used.
# Andrea Giammarchi (7 years ago)

gotta admit an if (let y = fn()) would be a very nice feature to have. Only the for(...) lets us declare block variables, the let expression would solve/simplify this case and many others.

const newFoo = (let x = getSomething()) ?
  (doSomethingFirst(), x.doSomething()) :
  doSomethingElse();
# Isiah Meadows (7 years ago)

This is starting to seem eerily familiar... (especially the idea of "let" expressions)

Take a look at this thread from a couple years ago, and you'll see what I mean.

esdiscuss.org/topic/the

# Andrea Giammarchi (7 years ago)

oh gosh, I've stopped at the title. I guess I'll just move on then ^_^

# Naveen Chawla (7 years ago)

I prefer the "do" approach (albeit in a different way, as I'm showing below). Can someone tell me why it's called "do" instead of something like "expr", or "eval" or something? "do" just seems weird naming for evaluating the last statement in a multi statement code block.

For the initial example, I prefer this use of the do concept, instead of the if else way of doing it:

const
    x = getSomething(),
    foo =
       x ?
          do {
              doSomethingFirst();
              x.doSomething()
          } :
          doSomethingElse()
# Isiah Meadows (7 years ago)

Haskell and most Lisp dialects have a do syntax that enables you to do multiple operations and return the result of the last. The difference between this and the comma operator is that you can use actual statements as well as expressions.

Also, my if/else usage was more just personal syntactic preference - a ternary works just as well.

# Naveen Chawla (7 years ago)

I had no idea about the comma operator! Can you give me an example of an "actual statement" that cannot be serviced in a comma operator expression?

As it stands then, I prefer the comma operator way for the initial example:

const
    x = getSomething(),
    foo =
       x ?
          (
              doSomethingFirst(),
              x.doSomething()
          ) :
          doSomethingElse()
# Andrea Giammarchi (7 years ago)

let's close the circle:

const newFoo = do { let x = getSomething(); x ?
  (doSomethingFirst(), x.doSomething()) :
  doSomethingElse();
};

and about this:

Can you give me an example of an "actual statement" that cannot be

serviced in a comma operator expression?

I guess you cannot throw or even try / catch / finally there ... which is just about it: any valid expression would do.

# Naveen Chawla (7 years ago)

The comma operator seems to make the do concept redundant, at least for me. Yes it forces ternaries like in my last example (as opposed to being able to express it via if else), but I prefer that anyway

# T.J. Crowder (7 years ago)

On Wed, Sep 20, 2017 at 11:28 AM, Naveen Chawla <naveen.chwl at gmail.com>

wrote:

The comma operator seems to make the do concept redundant, at least for me.

No, not at all. Again: With the comma operator, you can't use statements, only expressions. With the do expression, you can use actual statements:

const x = do {
    for (const x of getTheThings()) {
        if (x.id == something) {
            x.foo;
        }
    }
};

...which is basically:

const x = getTheThings().find(x => x.id == something).foo;

...except it doesn't throw if find returns undefined, and isn't a series of function calls.

do expressions are basically to address overly-complex conditional expressions and IIFEs. More: gist.github.com/dherman/1c97dfb25179fa34a41b5fff040f9879

-- T.J. Crowder

# Bob Myers (7 years ago)

Could you please clarify how the system would know that some random expression in the middle of something like this for/if construct should be treated as a sort of "return"? I can understand that the last expression in a block would be the return value, but how would it know that x.foo was an implicit return? By the way, in current iterations of the do concept is there a return?

const x = do {
    for (const x of getTheThings()) {
        if (x.id == something) {
            x.foo;
        }
    }
};
# T.J. Crowder (7 years ago)

On Wed, Sep 20, 2017 at 12:52 PM, Bob Myers <rtm at gol.com> wrote:

Could you please clarify how the system would know that some random expression in the middle of something like this for/if construct should be treated as a sort of "return"?

Sorry, my example was missing a break. And it may have been off, since the break (rather than x.foo) would be the last executed statement. The main point was: You can use statements in do expressions, not with the comma operator.

By the way, in current iterations of the do concept is there a return?

As far as I could tell reviewing the discussion hat the TC39 proposals list refers to, it hasn't been decided. As someone points out in the comments, with return it's effectively an IIFE. Folks seem quite interested in do expressions but I'm having trouble seeing much need for them in a world with IIFEs and arrow functions:

const x = (() => {
    for (const x of getTheThings()) {
        if (x.id == something) {
            return x.foo;
        }
    }
})();

I mean, yes, even with return the do expression version of that would be slightly more concise, but...

We're off-topic, though. This is about do expressions. The thread is about "result-forwarding ternary operators".

-- T.J. Crowder

# Andrea Giammarchi (7 years ago)

combining all the things (arrow with implicit return + arguments default + ternary)

const newFoo = ((x = getSomething()) => x ?
  (doSomethingFirst(), x.doSomething()) :
  doSomethingElse()
)();

So yeah, we probably don't need yet another pattern/new syntax to do that.

# Naveen Chawla (7 years ago)

As your example shows, for loops can be reduced to array reduction function calls. if elses can be represented by ternaries.

while loops aren't so straightforward but can be factored into single function calls. They are less common anyway.

I find single expressions listed as (a, b, c) more readable than code blocks who eventually evaluate to a single value each.

So, even if do expressions were introduced into ES, I would avoid using them as much as I could, unless it were really smarter and more readable to use them than any of the alternatives

# Michael Rosefield (7 years ago)

There was a suggestion that came up in my original reddit post that I think merits discussion here.

I know one of the main arguments against this new operator is to prevent needless and confusing language explosion, but u/notNullOrVoid pointed out that this bears similarity to another operator already under TC39 consideration, the pipeline-operator ( tc39/proposal-pipeline-operator).

My suggestion could then be implemented as follows:

const foo = getSomething() |> x => x ? dosomething(x) : doSomethingElse();

And this ternary could be simply a pipeline variant:

const foo = getSomething() ?> x => dosomething(x) : doSomethingElse();

That reads much clearer and also takes care of the syntactically invalid ?! formularion.

# Michał Wadas (7 years ago)

Actual characters are minor issue. There are many unused combinations of "special" characters. Eg. ?> ?: :? ?() @? ?* ?# ?@ @? -? =? ?= (?) etc.

# Michael Rosefield (7 years ago)

Oh, yes, but the real benefit here is that it consolidates this as a combination of the normal ternary operator and the piping operator; an organic synthesis of existing operators as opposed to an entirely new one grafted on to the language.

I'd go so far as to call it the 'piping ternary operator' and make it an extension of the piping operator proposal.

# Isiah Meadows (7 years ago)

Can we just let this thread die? The pipeline-based variant is only 2 tokens more than the shorthand, which provides no additional use. Additionally, the proposal as a whole is hardly useful outside some uncommon cases that already have workarounds.

# Naveen Chawla (7 years ago)

What I don't like is the callback part: x=>... for the 2nd operand. This is too specific. What if you want to forward something to a multi param call or just use it in the next code statement??? Therefore I think the power of this proposed operator is limited, compared to a straightforward ternary, an example of which is given for the use case you gave at the start:

const
    x = getSomething(),
    foo =
       x ?
          (
              doSomethingFirst(),
              x.doSomething()
          ) :
          doSomethingElse()

vs with your proposal:

const
    foo =
        getSomething() ?!
            x => {
                doSomethingFirst();
                return x.doSomething()
            } :
            doSomethingElse()

which appears to have equivalent code complexity to the ternary equivalent, at a loss of programmatic power as well as, in my view, understandability. Do tell me what I'm missing if you don't agree.