elves / elvish

Powerful scripting language & versatile interactive shell
https://elv.sh/
BSD 2-Clause "Simplified" License
5.69k stars 300 forks source link

Feature Suggestion: Supporting switch-case control flow #1361

Open simonrouse9461 opened 3 years ago

simonrouse9461 commented 3 years ago

Elvish doesn't currently support switch-case control flow, which in a lot of cases can be much more readable than if-else syntax.

One can simply emulate switch-case syntax by defining two functions like this:

fn case [@cases f]{
    put [value]{
        if (has-value $cases $value) { 
            $f 
        } else {
            fail __no_match__
        }
    }
}

fn switch [value case_fns @_else]{
    var matched = $false
    $case_fns | each [case_fn]{
        try {
            $case_fn $value
        } except {
            continue
        } else {
            set matched = $true
        }
    }
    if (and (not $matched) (> (count $_else) 0) (eq $_else[0] else)) {
        $_else[1]
    }
}

Then, this becomes possible:

switch 2 {
    case 2 3 5 {
        echo "small prime number"
    }
    case 2 4 6 {
        echo "small even number"
    }
} else {
    echo "something else"
}

which will output,

small prime number
small even number

This kind of syntax can be much cleaner and readable in certain cases than vanilla if-else. So, can we have two builtins switch and case to achieve similar syntax?

krader1961 commented 3 years ago

@simonrouse9461, I consider your example that matches "2" twice an anti-pattern. The Go language team deliberately decided not to follow the "C" language switch fall-through behavior. I would be extremely unhappy if Elvish behaved like the "C" language in this regard. Also, this can't be done via "builtins". It must be part of the syntax of the language.

simonrouse9461 commented 3 years ago

The Go language team deliberately decided not to follow the "C" language switch fall-through behavior. I would be extremely unhappy if Elvish behaved like the "C" language in this regard.

I see, but whether to allow fall-through is not my point here. If fall-through should be avoided, adding a break will fix it

fn switch [value case_fns @_else]{
    var matched = $false
    $case_fns | each [case_fn]{
        try {
            $case_fn $value
        } except {
            continue
        } else {
            set matched = $true
            break
        }
    }
    if (and (not $matched) (> (count $_else) 0) (eq $_else[0] else)) {
        $_else[1]
    }
}

Also, this can't be done via "builtins". It must be part of the syntax of the language.

Sorry if my words are misleading, but I don't actually mean it has to be builtins. I'm just suggesting to have this type of syntax emulated by these two functions regardless of how it is implemented under the hood.

xiaq commented 3 years ago

@simonrouse9461 I like your trick of implementing switch and case purely with function semantics :)

I also like how clean the syntax feels, but I want to avoid having two levels of indentation. The fact that the "normal" cases are indented differently also feels a bit awkward.

Another goal I have for switch is allowing specifying an alternative the matching function (the default can be eq). For example, == is useful when switching on a number, and re:match is useful when switching on a string. In the syntax you proposed this can be done by adding either an optional argument or a named option to switch.

xiaq commented 3 years ago

Hmm, if we (1) simply don't indenting the case clauses and (2) put the else clause inside the block, that would fix my complaints, but it looks weird, especially for the double closing } at the same level:

switch 2 {
case 2 3 5 {
    echo "small prime number"
}
case 2 4 6 {
    echo "small even number"
}
else {
    echo "something else"
}
}
simonrouse9461 commented 3 years ago

How about something like this:

switch 2 case 2 3 5 {
    echo "small prime number"
} case 2 4 6 {
    echo "small even number"
} else {
    echo "something else"
}

and with explicit matching function:

switch 2 [pattern value]{
    eq $pattern $value
} case 2 3 5 {
    echo "small prime number"
} case 2 4 6 {
    echo "small even number"
} else {
    echo "something else"
}

I know it's still a little weird, since the first case doesn't align with the rest in the first example. Or one can just use a ^ to force case to a new line?

switch 2 $re:match~ ^
case '.*'{1 3 5 7 9} {
    echo "odd number"
} case '.*'{2 4 6 8 0} {
    echo "even number"
} else {
    echo "something else"
}
zzamboni commented 3 years ago

For reference, I wrote an util:cond function which allows matching against a number of conditions in sequence. If a new builtin is added, maybe this would be a more generic way to do it?

See my implementation at https://github.com/zzamboni/elvish-modules/blob/master/util.org#conditionals

xiaq commented 3 years ago

How about something like this:

switch 2 case 2 3 5 {
    echo "small prime number"
} case 2 4 6 {
    echo "small even number"
} else {
    echo "something else"
}

and with explicit matching function:

switch 2 [pattern value]{
    eq $pattern $value
} case 2 3 5 {
    echo "small prime number"
} case 2 4 6 {
    echo "small even number"
} else {
    echo "something else"
}

I know it's still a little weird, since the first case doesn't align with the rest in the first example. Or one can just use a ^ to force case to a new line?

switch 2 $re:match~ ^
case '.*'{1 3 5 7 9} {
    echo "odd number"
} case '.*'{2 4 6 8 0} {
    echo "even number"
} else {
    echo "something else"
}

TBH I think they all look a bit weird... My current thinking is to just accept two levels of indentation.

The two-level syntax is kind of similar to Perl's given/when, and I'm ambivalent about whether that's an upside or downside :) (For those familiar with Perl, Elvish doesn't have the concept of topic variable and I don't intend to introduce it, so the resemblance is mostly superficial.)

For reference, I wrote an util:cond function which allows matching against a number of conditions in sequence. If a new builtin is added, maybe this would be a more generic way to do it?

See my implementation at https://github.com/zzamboni/elvish-modules/blob/master/util.org#conditionals

Nice :)

Clojure's expression-centered syntax style doesn't fit well with the rest of Elvish though, so I'll prefer the currently proposed switch/case syntax.

dumblob commented 3 years ago

If I may express my opinion, I like the syntax from @simonrouse9461 the best. I'm not sure whether it'd cause any parsing ambiguity issues, but I find it to be the least cluttered solution and the best for conveying the intention.

simonrouse9461 commented 3 years ago

If I may express my opinion, I like the syntax from @simonrouse9461 the best. I'm not sure whether it'd cause any parsing ambiguity issues, but I find it to be the least cluttered solution and the best for conveying the intention.

Thanks! I'm glad you like it. I feel one import merit of this syntax is that the structure is "flat", as it can be unrolled into a single line without messing up the readability, which makes it more suitable for being used as an interactive shell command.

simonrouse9461 commented 3 years ago

One problem of using a two-level structure is that it introduces a syntactical exception, where the code block accepted by the switch command cannot be a general lambda type, which obfuscates the syntactical role of curly braces. I worry that this may contaminate the clean and unified syntax as it currently is.