Proposal: switch expressions

David Koblas david at koblas.com
Thu Feb 28 17:28:25 UTC 2019


Isiah,

While the pattern-matching proposal does cover a much richer matching, 
it still doesn't target the issue of being a statement vs an 
expression.  Part of my initial motivation is that the evaluation of the 
switch returns a value, which pattern-matching doesn't resolve.

Very much enjoying the discussion,
David

On 2/28/19 12:07 PM, Isiah Meadows wrote:
>> Using a "switch" here forces you to group classes of objects together and then you don't get the 2nd, 3rd, 4th etc. levels of specialization that you might later want.
> Sometimes, this is actually *desired*, and most cases where I could've
> used this, inheritance was not involved *anywhere*. Also, in
> performance-sensitive contexts (like games, which *heavily* use
> `switch`/`case`), method dispatch is *far* slower than a simple
> `switch` statement, so that pattern doesn't apply everywhere.
>
> BTW, I prefer https://github.com/tc39/proposal-pattern-matching/ over
> this anyways - it covers more use cases and is all around more
> flexible, so I get more bang for the buck.
>
> -----
>
> Isiah Meadows
> contact at isiahmeadows.com
> www.isiahmeadows.com
>
> On Thu, Feb 28, 2019 at 9:23 AM Naveen Chawla <naveen.chwl at gmail.com> wrote:
>> Hi David!
>>
>> Your last example would, I think, be better served by classes and inheritance, than switch.
>>
>> Dogs are house animals which are animals
>> Cheetas are wild cats which are animals
>>
>> Each could have overridden methods, entirely optionally, where the method gets called and resolves appropriately.
>>
>> The input argument could be the class name, from which it is trivial to instantiate a new instance and get required results.
>>
>> Using a "switch" here forces you to group classes of objects together and then you don't get the 2nd, 3rd, 4th etc. levels of specialization that you might later want.
>>
>> All thoughts on this are welcome. Do let me know
>>
>> On Thu, 28 Feb 2019 at 14:06 David Koblas <david at koblas.com> wrote:
>>> Naveen,
>>>
>>> Thanks for your observation.  The example that I gave might have been too simplistic, here's a more complete example:
>>>
>>> ```
>>>
>>>      switch (animal) {
>>>      case Animal.DOG, Animal.CAT => {
>>>          // larger block expression
>>>          // which spans multiple lines
>>>
>>>          return "dry food";
>>>        }
>>>      case Animal.TIGER, Animal.LION, Animal.CHEETA => {
>>>          // larger block expression
>>>          // which spans multiple lines
>>>
>>>          return "fresh meat";
>>>        }
>>>      case Animal.ELEPHANT => "hay";
>>>      default => { throw new Error("Unsupported Animal"); };
>>>      }
>>>
>>> ```
>>>
>>> While you give examples that would totally work.  Things that bother me about the approach are, when taken to something more complex than a quick value for value switch you end up with something that looks like this.
>>>
>>> ```
>>>
>>>      function houseAnimal() {
>>>
>>>          // larger block expression
>>>          // which spans multiple lines
>>>
>>>          return "dry food";
>>>      }
>>>
>>>      function wildCatFood() {
>>>
>>>        // larger block expression
>>>        // which spans multiple lines
>>>
>>>        return "fresh meat";
>>>      }
>>>
>>>
>>>      const cases = {
>>>        [Animal.DOG]: houseAnimal,
>>>        [Animal.CAT]: houseAnimal,
>>>        [Animal.LION]: wildCatFood,
>>>        [Animal.TIGER]: wildCatFood,
>>>        [Animal.CHEETA]: wildCatFood,
>>>      }
>>>
>>>      const food = cases[animal] ? cases[animal]() : (() => {throw new Error("Unsuppored Animal")})();
>>>
>>> ```
>>>
>>> As we all know once any language reaches a basic level of functionality anything is possible.  What I think is that JavaScript would benefit by having a cleaner approach.
>>>
>>> On 2/28/19 4:37 AM, Naveen Chawla wrote:
>>>
>>> Isn't the best existing pattern an object literal?
>>>
>>> const
>>>      cases =
>>>          {
>>>               foo: ()=>1,
>>>               bar: ()=>3,
>>>               baz: ()=>6
>>>          }
>>>      ,
>>>      x =
>>>          cases[v] ?
>>>              cases[v]() :
>>>              99
>>> ;
>>>
>>> What does any proposal have that is better than this? With optional chaining feature:
>>>
>>> const
>>>      x =
>>>          {
>>>               foo: ()=>1,
>>>               bar: ()=>3,
>>>               baz: ()=>6
>>>          }[v]?.()
>>>          ||
>>>          99
>>> ;
>>>
>>> Do let me know your thoughts guys
>>>
>>>
>>> On Thu, 28 Feb 2019 at 06:04 kai zhu <kaizhu256 at gmail.com> wrote:
>>>> This is unmaintainable --
>>>>
>>>>      const x = v === 'foo' ? 1 : v === 'bar' ? 3 : v === 'baz' ? 6 : 99;
>>>>
>>>> i feel proposed switch-expressions are no more readable/maintainable than ternary-operators, if you follow jslint's style-guide.  i'll like to see more convincing evidence/use-case that they are better:
>>>>
>>>> ```javascript
>>>> /*jslint*/
>>>> "use strict";
>>>> const v = "foo";
>>>> const x = (
>>>>      v === "foo"
>>>>      ? 1
>>>>      : v === "bar"
>>>>      ? 3
>>>>      : v === "baz"
>>>>      ? 6
>>>>      : 99
>>>> );
>>>> ```
>>>>
>>>> here's another example from real-world production-code, where switch-expressions probably wouldn't help:
>>>>
>>>> ```javascript
>>>> $ node -e '
>>>> /*jslint devel*/
>>>> "use strict";
>>>> function renderRecent(date) {
>>>> /*
>>>>   * this function will render <date> to "xxx ago"
>>>>   */
>>>>      date = Math.ceil((Date.now() - new Date(date).getTime()) * 0.0001) * 10;
>>>>      return (
>>>>          !Number.isFinite(date)
>>>>          ? ""
>>>>          : date < 60
>>>>          ? date + " sec ago"
>>>>          : date < 3600
>>>>          ? Math.round(date / 60) + " min ago"
>>>>          : date < 86400
>>>>          ? Math.round(date / 3600) + " hr ago"
>>>>          : date < 129600
>>>>          ? "1 day ago"
>>>>          : Math.round(date / 86400) + " days ago"
>>>>      );
>>>> }
>>>>
>>>> console.log(renderRecent(new Date().toISOString())); // "0 sec ago"
>>>> console.log(renderRecent("2019-02-28T05:32:00Z")); // "10 sec ago"
>>>> console.log(renderRecent("2019-02-28T05:27:30Z")); // "5 min ago"
>>>> console.log(renderRecent("2019-02-28T05:14:00Z")); // "18 min ago"
>>>> console.log(renderRecent("2019-02-28T03:27:00Z")); // "2 hr ago"
>>>> console.log(renderRecent("2019-02-12T05:27:00Z")); // "16 days ago"
>>>> console.log(renderRecent("2018-02-28T05:27:00Z")); // "365 days ago"
>>>> '
>>>>
>>>> 0 sec ago
>>>> 10 sec ago
>>>> 5 min ago
>>>> 18 min ago
>>>> 2 hr ago
>>>> 16 days ago
>>>> 365 days ago
>>>>
>>>> $
>>>> ```
>>>>
>>>> On 27 Feb 2019, at 13:12, David Koblas <david at koblas.com> wrote:
>>>>
>>>> Just for folks who might be interested, added a babel-plugin to see what was involved in making this possible.
>>>>
>>>> Pull request available here -- https://github.com/babel/babel/pull/9604
>>>>
>>>> I'm sure I'm missing a bunch of details, but would be interested in some help in making this a bit more real.
>>>>
>>>> Thanks
>>>>
>>>> On 2/26/19 2:40 PM, Isiah Meadows wrote:
>>>>
>>>> You're not alone in wanting pattern matching to be expression-based:
>>>>
>>>> https://github.com/tc39/proposal-pattern-matching/issues/116
>>>>
>>>> -----
>>>>
>>>> Isiah Meadows
>>>> contact at isiahmeadows.com
>>>> www.isiahmeadows.com
>>>>
>>>> -----
>>>>
>>>> Isiah Meadows
>>>> contact at isiahmeadows.com
>>>> www.isiahmeadows.com
>>>>
>>>>
>>>> On Tue, Feb 26, 2019 at 1:34 PM David Koblas <david at koblas.com> wrote:
>>>>
>>>> Jordan,
>>>>
>>>> Thanks for taking time to read and provide thoughts.
>>>>
>>>> I just back and re-read the pattern matching proposal and it still fails on the basic requirement of being an Expression not a Statement.  The problem that I see and want to address is the need to have something that removes the need to chain trinary expressions together to have an Expression.
>>>>
>>>> This is unmaintainable --
>>>>
>>>>      const x = v === 'foo' ? 1 : v === 'bar' ? 3 : v === 'baz' ? 6 : 99;
>>>>
>>>> This is maintainable, but is less than ideal:
>>>>
>>>>     let x;
>>>>
>>>>     switch (v) {
>>>>     case "foo":
>>>>       x = 1;
>>>>       break;
>>>>     case "bar":
>>>>       x = 3;
>>>>       break;
>>>>     case "baz":
>>>>       x = 6;
>>>>       break;
>>>>     default:
>>>>       x = 99;
>>>>       break;
>>>>     }
>>>>
>>>> Pattern matching does shorten the code, but you have a weird default case and also still end up with a loose variable and since pattern matching is a statement you still have a initially undefined variable.
>>>>
>>>>     let x;
>>>>
>>>>     case (v) {
>>>>       when "foo" -> x = 1;
>>>>       when "bar" -> x = 3;
>>>>       when "baz" -> x = 6;
>>>>       when v -> x = 99;
>>>>     }
>>>>
>>>> Let's try do expressions, I'll leave people's thoughts to themselves.
>>>>
>>>>     const x = do {
>>>>       if (v === "foo") { 1; }
>>>>       else if (v === "bar") { 3; }
>>>>       else if (v === "baz") { 6; }
>>>>       else { 99; }
>>>>     }
>>>>
>>>> Or as another do expression variant:
>>>>
>>>>     const x = do {
>>>>       switch (v) {
>>>>         case "foo": 1; break;
>>>>         case "bar": 3; break;
>>>>         case "baz": 6; break;
>>>>         default: 99; break;
>>>>       }
>>>>     }
>>>>
>>>> And as I'm thinking about switch expressions:
>>>>
>>>>     const x = switch (v) {
>>>>       case "foo" => 1;
>>>>       case "bar" => 3;
>>>>       case "baz" => 6;
>>>>       default => 99;
>>>>     }
>>>>
>>>> What I really like is that it preserves all of the normal JavaScript syntax with the small change that a switch is allowed in an expression provided that all of the cases evaluate to expressions hence the use of the '=>' as an indicator.  Fundamentally this is a very basic concept where you have a state machine and need it switch based on the current state and evaluate to the new state.
>>>>
>>>>     const nextState = switch (currentState) {
>>>>        case ... =>
>>>>     }
>>>>
>>>>
>>>>
>>>> On 2/25/19 4:00 PM, Jordan Harband wrote:
>>>>
>>>> Pattern Matching is still at stage 1; so there's not really any permanent decisions that have been made - the repo theoretically should contain rationales for decisions up to this point.
>>>>
>>>> I can speak for myself (as "not a champion" of that proposal, just a fan) that any similarity to the reviled and terrible `switch` is something I'll be pushing back against - I want a replacement that lacks the footguns and pitfalls of `switch`, and that is easily teachable and googleable as a different, distinct thing.
>>>>
>>>> On Mon, Feb 25, 2019 at 12:42 PM David Koblas <david at koblas.com> wrote:
>>>>
>>>> Jordan,
>>>>
>>>> One question that I have lingering from pattern matching is why is the syntax so different?  IMHO it is still a switch statement with a variation of the match on the case rather than a whole new construct.
>>>>
>>>> Is there somewhere I can find a bit of discussion about the history of the syntax decisions?
>>>>
>>>> --David
>>>>
>>>>
>>>> On Feb 25, 2019, at 12:33 PM, Jordan Harband <ljharb at gmail.com> wrote:
>>>>
>>>> Additionally, https://github.com/tc39/proposal-pattern-matching - switch statements are something I hope we'll soon be able to relegate to the dustbin of history.
>>>>
>>>> On Mon, Feb 25, 2019 at 6:01 AM David Koblas <david at koblas.com> wrote:
>>>>
>>>> I quite aware that it’s covered in do expressions. Personally I find do expressions non-JavaScript in style and it’s also not necessarily going to make it into the language.
>>>>
>>>> Hence why I wanted to put out there the idea of switch expressions.
>>>>
>>>> --David
>>>>
>>>>
>>>> On Feb 25, 2019, at 5:28 AM, N. Oxer <blueshuk2 at gmail.com> wrote:
>>>>
>>>> Hi,
>>>>
>>>> This would be covered by do expressions. You could just do:
>>>>
>>>> ```js
>>>> const category = do {
>>>>    switch (...) {
>>>>      ...
>>>>    };
>>>> };
>>>> ```
>>>>
>>>> On Sun, Feb 24, 2019 at 10:42 AM David Koblas <david at koblas.com> wrote:
>>>>
>>>> After looking at a bunch of code in our system noted that there are many
>>>> cases where our code base has a pattern similar to this:
>>>>
>>>>       let category = data.category;
>>>>
>>>>       if (category === undefined) {
>>>>         // Even if Tax is not enabled, we have defaults for incomeCode
>>>>         switch (session.merchant.settings.tax.incomeCode) {
>>>>           case TaxIncomeCode.RENTS_14:
>>>>             category = PaymentCategory.RENT;
>>>>             break;
>>>>           case TaxIncomeCode.INDEPENDENT_PERSONAL_SERVICE_17:
>>>>             category = PaymentCategory.SERVICES;
>>>>             break;
>>>>           case TaxIncomeCode.INDEPENDENT_PERSONAL_SERVICE_17:
>>>>             category = PaymentCategory.SERVICES;
>>>>             break;
>>>>         }
>>>>       }
>>>>
>>>> I also bumped into a block of go code that also implemented similar
>>>> patterns, which really demonstrated to me that there while you could go
>>>> crazy with triary nesting there should be a better way.  Looked at the
>>>> pattern matching proposal and while could possibly help looked like it
>>>> was overkill for the typical use case that I'm seeing. The most relevant
>>>> example I noted was switch expressions from Java.  When applied to this
>>>> problem really create a simple result:
>>>>
>>>>       const category = data.category || switch (setting.incomeCode) {
>>>>         case TaxIncomeCode.RENTS_14 => PaymentCategory.RENT;
>>>>         case TaxIncomeCode.ROYALTIES_COPYRIGHTS_12 =>
>>>> PaymentCategory.ROYALTIES;
>>>>         case TaxIncomeCode.INDEPENDENT_PERSONAL_SERVICE_17 =>
>>>> PaymentCategory.SERVICES;
>>>>         default => PaymentCategory.OTHER;
>>>>       }
>>>>
>>>> Note; the instead of using the '->' as Java, continue to use => and with
>>>> the understanding that the right hand side is fundamentally function.
>>>> So similar things to this are natural, note this proposal should remove
>>>> "fall through" breaks and allow for multiple cases as such.
>>>>
>>>>       const quarter = switch (foo) {
>>>>         case "Jan", "Feb", "Mar" => "Q1";
>>>>         case "Apr", "May", "Jun" => "Q2";
>>>>         case "Jul", "Aug", "Sep" => "Q3";
>>>>         case "Oct", "Nov", "Dec" => { return "Q4" };
>>>>         default => { throw new Error("Invalid Month") };
>>>>       }
>>>>
>>>> Also compared this to the do expression proposal, it also provides a
>>>> substantial simplification, but in a way that is more consistent with
>>>> the existing language.  In one of their examples they provide an example
>>>> of the Redux reducer
>>>> https://redux.js.org/basics/reducers#splitting-reducers -- this would be
>>>> a switch expression implementation.
>>>>
>>>>       function todoApp(state = initialState, action) => switch
>>>> (action.type) {
>>>>         case SET_VISIBILITY_FILTER => { ...state, visibilityFilter:
>>>> action.filter };
>>>>         case ADD_TODO => {
>>>>             ...state, todos: [
>>>>               ...state.todos,
>>>>               {
>>>>                 text: action.text,
>>>>                 completed: false
>>>>               }
>>>>             ]
>>>>           };
>>>>         case TOGGLE_TODO => {
>>>>             ...state,
>>>>             todos: state.todos.map((todo, index) => (index ===
>>>> action.index) ? { ...todo, completed: !todo.completed } : todo)
>>>>           };
>>>>         default => state;
>>>>       }
>>>>
>>>>
>>>>
>>>> _______________________________________________
>>>> es-discuss mailing list
>>>> es-discuss at mozilla.org
>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>
>>>> _______________________________________________
>>>> es-discuss mailing list
>>>> es-discuss at mozilla.org
>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>
>>>> On 2/25/19 3:42 PM, David Koblas wrote:
>>>>
>>>> Jordan,
>>>>
>>>> One question that I have lingering from pattern matching is why is the syntax so different?  IMHO it is still a switch statement with a variation of the match on the case rather than a whole new construct.
>>>>
>>>> Is there somewhere I can find a bit of discussion about the history of the syntax decisions?
>>>>
>>>> --David
>>>>
>>>>
>>>> On Feb 25, 2019, at 12:33 PM, Jordan Harband <ljharb at gmail.com> wrote:
>>>>
>>>> Additionally, https://github.com/tc39/proposal-pattern-matching - switch statements are something I hope we'll soon be able to relegate to the dustbin of history.
>>>>
>>>> On Mon, Feb 25, 2019 at 6:01 AM David Koblas <david at koblas.com> wrote:
>>>>
>>>> I quite aware that it’s covered in do expressions. Personally I find do expressions non-JavaScript in style and it’s also not necessarily going to make it into the language.
>>>>
>>>> Hence why I wanted to put out there the idea of switch expressions.
>>>>
>>>> --David
>>>>
>>>>
>>>> On Feb 25, 2019, at 5:28 AM, N. Oxer <blueshuk2 at gmail.com> wrote:
>>>>
>>>> Hi,
>>>>
>>>> This would be covered by do expressions. You could just do:
>>>>
>>>> ```js
>>>> const category = do {
>>>>    switch (...) {
>>>>      ...
>>>>    };
>>>> };
>>>> ```
>>>>
>>>> On Sun, Feb 24, 2019 at 10:42 AM David Koblas <david at koblas.com> wrote:
>>>>
>>>> After looking at a bunch of code in our system noted that there are many
>>>> cases where our code base has a pattern similar to this:
>>>>
>>>>       let category = data.category;
>>>>
>>>>       if (category === undefined) {
>>>>         // Even if Tax is not enabled, we have defaults for incomeCode
>>>>         switch (session.merchant.settings.tax.incomeCode) {
>>>>           case TaxIncomeCode.RENTS_14:
>>>>             category = PaymentCategory.RENT;
>>>>             break;
>>>>           case TaxIncomeCode.INDEPENDENT_PERSONAL_SERVICE_17:
>>>>             category = PaymentCategory.SERVICES;
>>>>             break;
>>>>           case TaxIncomeCode.INDEPENDENT_PERSONAL_SERVICE_17:
>>>>             category = PaymentCategory.SERVICES;
>>>>             break;
>>>>         }
>>>>       }
>>>>
>>>> I also bumped into a block of go code that also implemented similar
>>>> patterns, which really demonstrated to me that there while you could go
>>>> crazy with triary nesting there should be a better way.  Looked at the
>>>> pattern matching proposal and while could possibly help looked like it
>>>> was overkill for the typical use case that I'm seeing. The most relevant
>>>> example I noted was switch expressions from Java.  When applied to this
>>>> problem really create a simple result:
>>>>
>>>>       const category = data.category || switch (setting.incomeCode) {
>>>>         case TaxIncomeCode.RENTS_14 => PaymentCategory.RENT;
>>>>         case TaxIncomeCode.ROYALTIES_COPYRIGHTS_12 =>
>>>> PaymentCategory.ROYALTIES;
>>>>         case TaxIncomeCode.INDEPENDENT_PERSONAL_SERVICE_17 =>
>>>> PaymentCategory.SERVICES;
>>>>         default => PaymentCategory.OTHER;
>>>>       }
>>>>
>>>> Note; the instead of using the '->' as Java, continue to use => and with
>>>> the understanding that the right hand side is fundamentally function.
>>>> So similar things to this are natural, note this proposal should remove
>>>> "fall through" breaks and allow for multiple cases as such.
>>>>
>>>>       const quarter = switch (foo) {
>>>>         case "Jan", "Feb", "Mar" => "Q1";
>>>>         case "Apr", "May", "Jun" => "Q2";
>>>>         case "Jul", "Aug", "Sep" => "Q3";
>>>>         case "Oct", "Nov", "Dec" => { return "Q4" };
>>>>         default => { throw new Error("Invalid Month") };
>>>>       }
>>>>
>>>> Also compared this to the do expression proposal, it also provides a
>>>> substantial simplification, but in a way that is more consistent with
>>>> the existing language.  In one of their examples they provide an example
>>>> of the Redux reducer
>>>> https://redux.js.org/basics/reducers#splitting-reducers -- this would be
>>>> a switch expression implementation.
>>>>
>>>>       function todoApp(state = initialState, action) => switch
>>>> (action.type) {
>>>>         case SET_VISIBILITY_FILTER => { ...state, visibilityFilter:
>>>> action.filter };
>>>>         case ADD_TODO => {
>>>>             ...state, todos: [
>>>>               ...state.todos,
>>>>               {
>>>>                 text: action.text,
>>>>                 completed: false
>>>>               }
>>>>             ]
>>>>           };
>>>>         case TOGGLE_TODO => {
>>>>             ...state,
>>>>             todos: state.todos.map((todo, index) => (index ===
>>>> action.index) ? { ...todo, completed: !todo.completed } : todo)
>>>>           };
>>>>         default => state;
>>>>       }
>>>>
>>>>
>>>>
>>>> _______________________________________________
>>>> es-discuss mailing list
>>>> es-discuss at mozilla.org
>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>
>>>> _______________________________________________
>>>> es-discuss mailing list
>>>> es-discuss at mozilla.org
>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>
>>>>
>>>> _______________________________________________
>>>> es-discuss mailing list
>>>> es-discuss at mozilla.org
>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>
>>>> _______________________________________________
>>>> es-discuss mailing list
>>>> es-discuss at mozilla.org
>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>
>>>> _______________________________________________
>>>> es-discuss mailing list
>>>> es-discuss at mozilla.org
>>>> https://mail.mozilla.org/listinfo/es-discuss
>>>>
>>>>
>>>> _______________________________________________
>>>> es-discuss mailing list
>>>> es-discuss at mozilla.org
>>>> https://mail.mozilla.org/listinfo/es-discuss
>> _______________________________________________
>> es-discuss mailing list
>> es-discuss at mozilla.org
>> https://mail.mozilla.org/listinfo/es-discuss


More information about the es-discuss mailing list