prql icon indicating copy to clipboard operation
prql copied to clipboard

switch / case / match semantics

Open max-sixty opened this issue 2 years ago • 19 comments

As discussed on Discord, we have a fully functioning switch statement from #1278 thanks to @aljazerzen, and there are a couple of options on how this is done:

Conditions

switch [
  score <= 10 -> "low",
  score <= 30 -> "medium",
  score <= 70 -> "high",
  true -> "very high",
]
  • Advantage — anything is representable in a reasonable way
  • This is the current approach in main

Compare against a value

switch score [
  <= 10 -> "low",
  <= 30 -> "medium",
  <= 70 -> "high",
  _ -> "very high",
]
  • Advantage — visually simpler and more concise (personally I really like the thinking process of "OK we care about foo, no need to learn that on each subsequent line")
    • Comparing a single expression to multiple possible values in a very common use of a logic branch like this
  • Disadvantages:
    • We probably need to adjust our parser to allow <= foo as a valid expression — does everything become a UnOp?
    • Representing a full condition is possible but inelegant ("OK we care about true"??)
      switch true [
        score <= 10 -> "low",
        status == "exempt" -> "exempt",
      ]
      
      ...since status == "exempt" is compared to true.

Allow an optional condition

Allow either of these. This seems quite compelling, and there's a track-record from other languages. But there are some tradeoffs:

  • We'd need to use a named parameter
    switch on:score [
      <= 10 -> "low",
      <= 30 -> "medium",
      <= 70 -> "high",
      _ -> "very high",
    ]
    
  • We still pay the costs of adjusting the parser to alow for <= 10 as a valid expression.
    • ...or allow for optional positional parameters (which we don't currently do). I'm not entirely opposed to this at some point, though it does make our parameter logic more complicated. Allowing "optional positional parameters with a default of null" doesn't complicate the language much, but isn't sufficient here, since the optional parameter would come first, meaning we'd need something like multiple dispatch. Type-checking and autocomplete could become much more difficult.

Naming

SQL calls this case...when...then, quite the coalition...

Defaults

I rather like the approach of defaulting to null (which is what the current implementation does). We can't have exhaustive pattern matching anyway, so I'm not sure there's much cost.

max-sixty avatar Dec 17 '22 23:12 max-sixty

From the proposed syntaxes I like switch on: the most.

An alternative would be a separate match statement:

match x [
   ...
]

Regarding the parsing problem: we can get around that by using patterns:

match x [
    1 -> 'one'
    2..5 -> 'a few',
    5.. -> 'many'
]

We'd have to support ranges for characters, numbers and maybe strings.

aljazerzen avatar Dec 18 '22 11:12 aljazerzen

Regarding the parsing problem: we can get around that by using patterns:

match x [
    1 -> 'one'
    2..5 -> 'a few',
    5.. -> 'many'
]

We'd have to support ranges for characters, numbers and maybe strings.

Yes interesting, that does work for lots of cases (similar to rust I guess). What would the semantics be here? "Use x | in y where x is the matched value and y is the value on the left side of the match branch"?

Because we can't do proper pattern matching, we don't give as much up by having semantics of "just put x at the beginning of the expression", like:

match x [
    == 1 -> 'one'
    | in 2..5 -> 'a few',
    | in 5..10 -> 'many'
    != null -> 'out of bounds'
    == null -> 'null'
]

though we could have guards so we get something similar with the slicker syntax of the original suggestion:

match x [
    1 -> 'one'
    2..5 -> 'a few',
    5..10 -> 'many'
    _ if x != null -> 'out of bounds'
    null -> 'null'
]

(Something I just found with rust — if x is itself a range 2..5, then matching on it won't work, because 2..5 matches against a range of ints, I guess at the language level, not against the value of x)


Possibly the "closest" option is to allow switch on:x, do the == 1 -> 'one' option, and then later add a match statement. (but no strong view at all — all of these have tradeoffs)

max-sixty avatar Dec 18 '22 22:12 max-sixty

Yes, the semantics of:

match x [
  cond1 -> val1
  .. -> val2
]

would be the same as of:

switch [
   x | in cond1 -> val1
   true -> val2
]

I don't like the guard too much, because it's heavy in additional syntax while it could be rewritten into:

match x [
    1 -> 'one'
    2..5 -> 'a few',
    5..10 -> 'many',
    _ -> switch [
        x != null -> 'out of bounds',
        true -> 'null',
    ]
]

Yes, this is not as clear as the "match with if guard", but for now, I think it's enough. I'm not keen on adding too much features that would bloat the language.

aljazerzen avatar Dec 20 '22 08:12 aljazerzen

I'm in favour of keeping the current switch using conditions because conditions can reference different variables/columns as you fall through the ladder which can be useful (and is allowed by the underlying SQL).

I also support introducing a separate match statement for the common case where you want to compare a single column/expression against multiple values/conditions.

(Putting thoughts/ideas on match syntax in a separate reply so you can easily downvote/comment on that.)

snth avatar Dec 20 '22 13:12 snth

Would it be possible to extend the match syntax so that it's not just for the in function but any function?

match x [
  in range1 -> val1
  func2 -> val2
  .. -> val3
]

would be the same as:

switch [
   x | in range1 -> val1
   x | func2 -> val2
   true -> val3
]

Then if any match condition involving _ could be rewritten into a lambda, e.g. _ > 5 becomes func _lambda1 _ -> _ > 5 then you could write

match x [
  _ > 5 -> val2
  .. -> val3
]

would become

switch [
   x | (func _lambda1 _ -> _ > 5`) -> val2
   true -> val3
]

Of course there's not much gained over simply using switch when your match expression is simply x but when you're matching instead on something like (temp_fahrenheit - 32) / 1.8 or more complicated then that is quite a saving.

Of course this goes against "not introducing too many features into the language".

snth avatar Dec 20 '22 13:12 snth

Actually, given that we don't have late binding for functions, I guess my lambda acrobatics above wouldn't work for the common case of

match col1 [
  _ > col2 -> val2,
  ... -> val3
]

since that would become

switch [
   col1 | (func _lambda1 _ -> _ > col2`) -> val2
   true -> val3
]

Better just to rewrite the expression involving the _ if you were to go that route.

snth avatar Dec 20 '22 14:12 snth

Interesting "pattern is a function" idea. I really like the functional aspect of it.

Rewriting _ into lambda can be a separate feature that can work in general (and it would be useful in pipelines).

We can start with this:

match x [
  in foo -> val1,
  eq bar -> val2,
  func2 -> val3,
  default -> val4,
]
# ... is equivalent to:
switch [
  (x | in foo) -> val1,
  (x | eq bar) -> val2,
  (x | func2) -> val3,
  (x | default) -> val4,
]

# ... where default is defined as:
func default x -> true
func eq a b -> a == b

For this to be ergonomic, we'd need a special impl for std.in:

func in pattern val -> {
   # internal compiler implementation
   if low..high = pattern {
     `val BETWEEN low and HIGH`
   } else if low.. = pattern {
     `val >= low`
   } else if ..high = pattern {
     `val <= high`
   } else if .. = pattern {
     `true`
   } else if list = pattern {
     `val IN (list)`
   } else {
      raise error
   }
}

Original example could be rewritten as:

match score [
  in ..10 -> "low",
  in ..30 -> "medium",
  in ..70 -> "high",
  eq null -> "missing",
  default -> "very high",
]

aljazerzen avatar Dec 21 '22 11:12 aljazerzen

Cool. The _ lambda syntax is inspired / taken from this fn.py python package.

I've used it before and find it a pretty convenient way to write lambdas.


I have some reservations about the naming of the default function. I know it's standard to have the catch all clause in switch statements called "default" but given that this would be a globally available function, I worry that it might lead to unforeseen problems elsewhere. (For example I recall another discussion about aggregate functions returning default / identity elements when given of empty lists which would be a conflicting meaning of default.)

Some ideas for alternative names:

  • other
  • catch_all
  • true (if you can have a function and constant with the same name that the compiler disambiguates based on type signature)
  • or_true (inspired by the Rust or_else and and_then type combinators. Would go with another one called and_false which always returns false.)

If we're going to go with eq then we might as well go with all the other standard comparison operators as well (ne, gt, ge, lt, le) which I think are relatively well known from Python operator module, Bash Comparison operators, HTML Character Escape Codes.


Actually, from looking at your std.in implementation, I see you're already catering for a in .. case mapping to true, so with three extra cases you could probably get the whole thing done with the in function:

   } else if low<.. = pattern {
     `val > low`
   } else if ..<high = pattern {
     `val < high`
   } else if value = pattern {
     `val == value 
   }

and then the example would be:

match score [
  in null -> "missing",
  in ..<0 -> "negative",
  in ..10 -> "low",
  in ..30 -> "medium",
  in ..70 -> "high",
  in 100<.. -> "very high",
  in .. -> "NaN",
]

I don't know if that would really be my preferred approach from a readability perspective, just saying you're almost there already with what you sketched out.

For comparison the lambda example would look something like the following:

match score [
  _==null -> "missing",
  _<0 -> "negative",
  _<=10 -> "low",
  _<=30 -> "medium",
  _<=70 -> "high",
  100<_ -> "very high",
  or_true -> "NaN",
]

Actually the previous case with just the in functions looks pretty neat. in null is probably the most awkward of the bunch and you could get rid of that by removing the equality case and just forcing people to use a single element list, i.e.

match score [
  in [null] -> "missing",
  in ..<0 -> "negative",
  ...
  in .. -> "NaN",
]

snth avatar Dec 21 '22 12:12 snth

The previous examples don't really showcase the benefits of the functional approach since they are mostly all just range matches which can be done easily with standard comparison operators. However if you had more complicated functions then the approach of matching against function evaluations might really make things more readable, for example something like

derive [
  compensation_factor = match date [
    is_workday -> 1,
    is_saturday -> 1.5,
    is_public_holiday -> 2,
    in .. -> null,
    ]
  ]

snth avatar Dec 21 '22 13:12 snth

I have some reservations about the naming of the default function.

I see. We can hold off from defining default and use in .. as you suggested.


If we're going to go with eq then we might as well go with all the other standard comparison operators as well

Sure, I doubt there is any cost to this (unlikely column/table names).


   ...
   else if value = pattern {
     `val == value 
   }

I initially wanted to do this, but it doesn't make much sense to say "match x in null". So I prefer match x eq null.

aljazerzen avatar Dec 22 '22 09:12 aljazerzen

Thanks a lot for #1332.

I really like the approach of passing the arg into an implicit function!

I'm trying to think if we can keep the syntax of the simple & most common case of "equal to this value" to be simple. I'm not a great fan of the functional form of the infix operators like eq — I'm imagining explaining to someone when they have to write eq rather than == and it being confusing.

Maybe there's a rule like "if the line starts with an infix operators such as ==, then the argument is on its left. Otherwise it's passed into an implicit function".

I'm reminded of Swift's approach of minimizing syntax where possible and unambiguous — the first half of https://docs.swift.org/swift-book/LanguageGuide/Closures.html

Let me think more over the weekend.

max-sixty avatar Dec 24 '22 07:12 max-sixty

That's a valid concern: eq, lte, lt, ... are not ideal.

I don't like having == without left expr, because it's not convenient to parse and introduces new sytnax. Current approach is great on this side: new syntactic constructs are high-level and are composed from normal existing exprs.

Maybe we could have the special rule "if the condition is not a function, just use equals comparison".

With this we'd cover only eq and leave out lt, lte, gt, gte and neq. But eq is most common by far and most of them can be expressed by ´in range´

aljazerzen avatar Dec 24 '22 11:12 aljazerzen

Maybe we could have the special rule "if the condition is not a function, just use equals comparison".

Yes this sounds reasonable!

Is there an ambiguity when we want to say "is equal to the result of this function"?

match x [
  5 -> "five"
  add 3 3 -> "six"  # tries to do `add 3 3 x` and fails
]

...but possibly that's no big deal, this wouldn't be allowed, instead functions need to be pre-computed in a derive.

With this we'd cover only eq and leave out lt, lte, gt, gte and neq. But eq is most common by far and most of them can be expressed by ´in range´

Agree, this is completely fine for the lt etc. neq is an inconvenience but survivable.


I don't like having == without left expr, because it's not convenient to parse and introduces new sytnax.

This is fair, though one thing to note — == is already a unary operator from join [==id]. But != & >= & friends would indeed need parsing changes.


Something I realized while thinking about this is that Rust also has a small ambiguity:

    let x = 0..5;
    match x {
        0..5 => true,   # doesn't match based on equality of `0..5 == 0..5` like any other match
        _ => false
    }

...fails because ranges are special.


Not to expand the scope of this needlessly — but worth raising that these plans commit us to having in as a function, rather than an infix operator like and and or. Do we want that?

Whether things like in or as are infix or functions has no Correct answer — it's the usual tradeoff between convenience and consistency.

It's possible to have in like x in 5..10. This ingrains in in the language — it's not possible for standard functions to be infix — but we're already doing that.


[Speculative, need to think more]

So maaaybe there's a synthesis of this where in becomes infix, and match statements need to start with an infix operator. And either we disallow functions for consistency, force a pipe symbol at the start, or add a pipe symbol implicitly for convenience.

max-sixty avatar Dec 25 '22 17:12 max-sixty

This is fair, though one thing to note — == is already a unary operator from join [==id].

This is actually a conflict between the two meanings. If use ==column in match, it this a unary operator or the binary with first arg omitted?


Something I realized while thinking about this is that Rust also has a small ambiguity:

If go for "value in match implies equality operator" semantics, we will also have a clash like this: you wouldn't be able to compare functions. But this doesn't make sense anyway, so it's not a problem.


these plans commit us to having in as a function

This is true and I don't like its current function signature. The fact that these two are idiomatic is not too appealing:

(x | in 1..5)
in 1..5 x

Another idea is to make all binary operations just normal functions (and maybe unary also).

Like have something like:

func binary in left right -> <built-in>
func binary + left right -> <built-in>

x in y
a + b

This is a major breaking change, because it then requires much more parenthesis:

derive 5 + 6
# would be parsed as:
FuncCall
- function: derive
- args: [5, +, 6]

derive (5 + 6)
# would be parsed as:
FuncCall:
  function: derive
  args:
  - FuncCall:
      function: +
      args: [5, 6]

This is a major inconvenience and also has some problems with resolving, so it more of an example of what we want to stay away from.

Essentially, binary operators are a more convenient, built-in way of calling functions. Their "names" are hardcoded into the parser and usually non-alphabetic symbols (+, %, <=) but not always (and, or).

Our problem right now is "how to call these operators with an implicit argument". And proposed solutions were:

  • define normal functions for each of the operators and use currying,
  • syntax for constructing lambda functions as _ == 4,
  • (only in match conditions) allow omitting the leading operand in binary ops.

aljazerzen avatar Dec 26 '22 11:12 aljazerzen

This is fair, though one thing to note — == is already a unary operator from join [==id].

This is actually a conflict between the two meanings. If use ==column in match, it this a unary operator or the binary with first arg omitted?

I think the rules here would be "if there's a comparison operator, then use as a binary operator. Otherwise add an implicit pipe". So unary operators would always convert to binary operators when at the beginning of a match.

...which would indeed mean we'd need to allow comparison operators at the beginning of those expressions, which does require changing parsing a bit. But I think it would be manageable.


I can see a few alternatives here:

  • Status quo — we have the current binary operators built into the compiler
  • Extend infix [^1] operators slightly — i.e. as & in. Rust & Python do the same.
    • If we thought it were much simpler to understand, we could say "Symbols are infix, and only symbols", and use symbols for and & or & in (& as?).
  • Allow for more flexibility between infix & prefix operators, e.g. func binary + left right -> <built-in>

[^1]: I'm using "binary functions" & "infix operators" analogously. I think in APL — where they've thought a lot about this — they're similar but slightly different in some way... https://aplwiki.com/wiki/Dyadic_function]

I agree the final one is fraught with peril — we're trying to keep things simple, which sometimes requires thinking about things at a deeper level — but in this case I don't feel qualified to suggest changes at this level that would make things simpler overall.


(only in match conditions) allow omitting the leading operand in binary ops.

So I think I would vote for this — it seems quite simple to explain, doesn't complicate the parser much, and avoids having to define how things like currying would work for infix operators.

WDYT?

max-sixty avatar Dec 28 '22 00:12 max-sixty

(only in match conditions) allow omitting the leading operand in binary ops.

If I expand this option a little, the syntax would look like:

match score [
  <= 10 -> "low",
  in ..30 -> "medium",
  <= 70 -> "high",
  != null -> "very high",
  in .. -> "missing",
]

Am still hesitant on having this unbalanced binary operations... They just don't fell right (although I may just get used to them).

There are now also two ways of doing comparison - which is a basic operation and should have only one idiomatic representation.

I cannot decide which of the options I like best or even which one I dislike the least.

aljazerzen avatar Dec 28 '22 10:12 aljazerzen

There are now also two ways of doing comparison - which is a basic operation and should have only one idiomatic representation.

Definitely agree with the principle.

  • I'm less concerned that both in and >= work — that's the case with comparisons at the moment, and we can lint them into a single standard.
  • Having the cases work in two different ways is not ideal though. It's two, rather than some long discovery of edge cases, but still not ideal

I cannot decide which of the options I like best or even which one I dislike the least.

😆

We can always leave it for a bit. At some point we should start wrapping up 0.4, and maybe this is something we let drag over to 0.5 if we don't have a clear view? (Generally a fan of shipping & iterating, but if it's not blocking and we're not spending time on it, fine to leave!)

max-sixty avatar Dec 28 '22 21:12 max-sixty

One more choice to add to the buffet: do we want this to be match or switch on:.

As part of my campaign to limit keywords, I would vote for switch on:, but not super-strongly, and as ever deferring to the do-ocracy.

max-sixty avatar Dec 29 '22 18:12 max-sixty

With the current impl of match, it is not really much different than switch. When compared with pattern matching in Rust, this is a mere syntactic sugar for if/switch.

If we are to improve our type system, exhaustive pattern matching could be possible and may make much more sense. But we cannot know that now, as we don't have the type system yet.

So my suggestion is to postpone this until we have adequate type system.

aljazerzen avatar Jan 09 '23 09:01 aljazerzen

I agree with the proposal to leave match for 0.5 or later and rather get 0.4 out with switch because that's a big win and enables a whole lot of new queries. As you pointed out, the proposed match would just be syntactic sugar and doesn't enable anything fundamentally new like switch.


Is there an ambiguity when we want to say "is equal to the result of this function"?

This actually alerted me to the fact that we're currently overloading -> for both function definitions and switch (and possibly match) clauses. Why don't we rather use => for switch like Rust does for match seeing that Rust also makes this distinction also using -> for function return types?

Perhaps this is not a problem for the parser at the moment but given that we may want to use -> for lambda function definitions (as alluded to in #523) then maybe this might become a problem later?

snth avatar Jan 10 '23 08:01 snth

then maybe this might become a problem later?

You're right, this will become a problem if we wanted -> for lambda functions.

aljazerzen avatar Jan 10 '23 12:01 aljazerzen

Perhaps this is not a problem for the parser at the moment but given that we may want to use -> for lambda function definitions (as alluded to in #523) then maybe this might become a problem later?

Yes good point.

My guess is that it will be "fudge-able" in the compiler. Because we won't be defining functions in a list, and won't be matching on lambda functions. And I don't think it will be confusing for users.

There's also a nice corollary between the two things: "give me the thing on the left and I'll give you the thing on the right":

# (currently `func add a b -> a + b`)
let add = a b -> a + b
switch x [5 -> "five"]

But I'm def open to the point that "these things are similar but not the same, and so having the same syntax is more confusing". Or that we'll hit issues in the compiler.

max-sixty avatar Jan 10 '23 19:01 max-sixty

Looking at the example from here https://github.com/PRQL/prql/issues/1564#issuecomment-1387556359

# from playground
from tracks
select [
  category = switch [
    length > avg_length -> 'long'
  ]
]
group [category] (
  aggregate (count)
)

I'm +1 now changing this to => for switch.

# from playground
from tracks
select [
  category = switch [
    length > avg_length => 'long'
  ]
]
group [category] (
  aggregate (count)
)

snth avatar Jan 18 '23 21:01 snth

What's the logic for the extra arrow type?

That functions and switch expression are sufficiently different and so should have different syntax?

I think we should have as little different syntax as possible, as long as it's not ambiguous — so we should need good reasons for adding a type of syntax.

For example, we use sort [-col], not sort [\col], because we already have a minus sign, and it's sufficiently clear, even though not quite as clear as the downward sloped slash would be.

max-sixty avatar Jan 19 '23 18:01 max-sixty

Those are good questions @max-sixty and I agree that as a general rule we should have as little different syntax as possible.

I expressed my sentiment/vote, probably largely on feeling and observation but I will try to do my best to reason this out as far as possible. @max-sixty I think you actually already listed some of the reasons:

And I don't think it will be confusing for users.

I think it will be confusing, similarly to when people were asking, why can I do sort cost but not sort -cost (which has to be sort [-cost]. I wanted to weigh in on that conversation but didn't get to (yet, not sure if it's still open). I would vote for always requiring the [] as I think it would make simpler in people's minds. In this case, knowing that switch always takes => in my opinion will be simpler for users and they don't have to wonder "why doesn't this trigger a lambda definition" and have to think about how the compiler is context aware ...

But I'm def open to the point that "these things are similar but not the same, and so having the same syntax is more confusing".

I agree with this.

Or that we'll hit issues in the compiler.

That was also a concern of mine but one I can't really comment. However @aljazerzen in a follow up comment said that he thought this would become an issue. I'm actually less concerned though about this because there is a lot that compilers can easily understand that humans can't (e.g. look at code golf examples).

That functions and switch expression are sufficiently different and so should have different syntax?

Not really.

For me, in my last example it mostly came down to that I don't think it reads well.

  category = switch [
    length > avg_length -> 'long'
  ]

Scanning it, my eye first hit the -> and I thought "oh there's a function definition" which then goes into "that doesn't make sense there", then I scan out and see that it's a switch so I try to reparse it and now it looks like blah > blah > blah so it looked like two greater than signs in a row because the minus is quite easy to miss. Of course then you go through it slowly and it all makes sense but it's not as convenient as it could be. To me it goes against the "Don't make me think" maxim of design.

"So @snth, you're saying that -> looks too much like >?" "I guess ... kinda ... yes." "So won't that be an issue for lambda definitions as well?" "Hmmm.... let me think". Actually no, I don't think so because there you'll only have identifiers on the LHS whereas in a switch expression you'll often have comparison operators.

Overall I think the following just reads much better because the double lines in => are much stronger:

  category = switch [
    length > avg_length => 'long'
  ]

What about when you have a == on the left? Good point, I don't know. Let's try it and see:

  category = switch [
    length == avg_length => 'avg'
  ]

Still seems fine to me, whereas

  category = switch [
    length == avg_length -> 'avg'
  ]

I don't know, ... it just seems to lack enough gravitas or strength to "move" you to the expression on the right 🤷‍♂️ Does that make any sense? I know it's a bit wishy washy and not really scientific or measurable but if I try to explain it, I think that's kinda what I based my vote on.

For me it's just about creating the most beautiful language that's the most fun to use and this seemed better. Plus, Rust does it that way so there's that. 😀

snth avatar Jan 19 '23 20:01 snth

I don't know, ... it just seems to lack enough gravitas or strength to "move" you to the expression on the right 🤷‍♂️ Does that make any sense? I know it's a bit wishy washy and not really scientific or measurable but if I try to explain it, I think that's kinda what I based my vote on.

lol, I mean, I wouldn't call it "wishy washy" exactly — but it does sound like it's based on personal aesthetic taste. To some extent that's a lot of what we have with some of these decisions, so it's reasonable to put some weight on it.

The other factors I would weigh are consistency with the rest of the language and familiarity for new users.

Consistency:

  • Having "arrow" mean something when you're describing a piece of code to someone — rather than "equals greater than" or "fat arrow", or even "switch symbol" — is important.
  • I am totally ambivalent between -> and => for our arrow syntax, so we could switch (pun intended) both

Familiarity

  • rust indeed uses => for the switch equivalent, and -> for return types. I tried to find a discussion where someone described why they were different — I think something about it being necessary for the original parser (but until I can find it, we shouldn't put much weight on the point).
  • Otherwise, I think C# & JS uses => for their function syntax.

One note re voting:

I think that's kinda what I based my vote on.

The way I think about the voting is that we're each responsible for matching the weight of our vote to the strength of our convictions. So it's not like we all are forced to choose a side and then we add up round numbers — if one person is adamant and two don't care much but slightly contrary, we should go with the one person's choice. That ofc relies on us all reporting the strength of our conviction honestly, and our convictions being well-calibrated — I hope we have that trust of each other (I certainly do)

max-sixty avatar Jan 19 '23 23:01 max-sixty

I've merged the PR firstly because the -> syntax would probably clash with lambda function syntax. There would probably be a way around that (parenthesis or just some kind of precedence rule). Second reason were personal aesthetics, which are pointless to discuss.

Overall, I'm +0.5 on =>.

aljazerzen avatar Jan 20 '23 08:01 aljazerzen

What do you think about having both be => @aljazerzen ? Or you're still concerned about the clashing?


Re the clashing — with enough context, there's no ambiguity, we're never going to match onto a lambda:

switch on:x (
  5 -> "foo"
  (a b -> a + b) -> "bar"  # wait wut
)

but:

  • the less context we require to parse correctly, the simpler and more general the parser can be
  • to some extent, we see that with = being used for both aliases and assigns, which means we can't have "an assign containing a function call outside a list" (super complicated!)

Assuming we go with both -> and => what would we call these? "Thin arrow" & "Fat arrow"?

max-sixty avatar Jan 21 '23 20:01 max-sixty

I'm still +1 on having different arrows (what's our scale again? In my mind it's -2..2, not -1..1).


Assuming we go with both -> and => what would we call these? "Thin arrow" & "Fat arrow"?

I think "Thin arrow" & "Fat arrow" are good names!

+1

snth avatar Jan 22 '23 10:01 snth

Second reason were personal aesthetics, which are pointless to discuss.

I think personal aesthetics are important and are ok to discuss. Where the danger lies, and why often it's a good idea to avoid them, is when I try to convince you that my aesthetics are better than yours. Another danger would be to argue for something with technical reasons when it's really motivated by personal aesthetics and that's being hidden. I think that goes with @max-sixty 's point about trust.

I tried to be very transparent in my description to differentiate where I saw technical merits and what I were aesthetical motivations for that reason. In a way I tried to make it easy to shoot down my arguments because I know in technical communities like ours there's not always a lot of weight placed on aesthetics.


Just on aesthetics, I think they are very important. It's hard to draw a line between ergonomics (which are perhaps a bit more technical) and aesthetics (which are probably more personal). However I believe that ergonomics and aesthetics can be a big factor in whether a language becomes popular or not.

I first used Python in 2000 and seriously in 2005 (I think it was 2.3 or 2.4 back then). There was no Pandas, no Numpy (I think the precursor Numeric was around) and Python was largely seen as a scripting language. The main competitors were Perl, Bash and Ruby.

Perl was arguably more ergonomic and you could write very terse things. However I believe Python largely won out, due to some of the following (IMHO):

  • Perl was a write-only language (good luck trying to decipher those concise hieroglyphics a few weeks later)
  • Python was fun to write and readable
  • People worked to bring things like Numpy and Pandas to the language because they enjoyed working in the language and wanted to do more with it.

I don't know what a good way is to bring good aesthetics to a language without ending up in endless bike shedding/yak shaving discussions. Perhaps there can be easy wins though if we all share our personal aesthetic preferences and where they agree then that's probably a good indication that we're onto something good. If there is large disagreement then we just need to take care that we stop any discussions before they get too protracted.

snth avatar Jan 22 '23 10:01 snth