DanielXMoore / Civet

A TypeScript superset that favors more types and less typing
https://civet.dev
MIT License
1.36k stars 29 forks source link

Pattern matching to-do #335

Open edemaine opened 1 year ago

edemaine commented 1 year ago

(transcribing these from Discord)

STRd6 commented 1 year ago

For class and multiple of the same keys we should have some way of aliasing but I'm not sure of the best syntax for it yet.

edemaine commented 1 year ago

Perhaps {class: klass === "hi"} or {class: klass is "hi"}? Eh, maybe that looks like an expression that should be evaluated and passed in as the class value to match.

I think we also need to improve the default behavior when there are duplicate or keyword names. Some options:

  1. Keyword names could get an automatic _ or 1 suffix or something.
  2. Just the first instance of a name could get a variable.
  3. First instance gets a 1 suffix, second gets a 2 suffix, etc.
  4. Name gets assigned an array of values (but this changes substantially between between one and multiple instances of the name). [as you've suggested in Discord]
  5. Name with suffix of 0 gets an array of matches, like how in Hera $0 is [$1, $2, ...].

I also feel like the most common case of duplicate names is when we have bound them to constants already. Something that would help in this sort of scenario is to not actually bind variables if they don't get used in the scope of the match. This would also pair nicely with Option 5 above; we'd like to build this array only if it gets used.

edemaine commented 1 year ago

375 implemented Option 4 above, which I agree seems like a good choice.

A simple workaround for class and other keywords, at least for now, would be to just not bind those (or use a Ref that the user can't access). At least then the code would compile.

eevleevs commented 1 year ago

What about adding the same functionality as in #444? To allow something like:

switch a
  m := /(\d)/
    return m[1]
JUSTIVE commented 10 months ago

any plans for guard clauses other than simple binary operators?

edemaine commented 10 months ago

@JUSTIVE Yes! I don't think we've settled on a notation yet, but I also may be forgetting a proposal. One obvious thing to allow would be & shorthand functions:

switch x
  & > 0        // equivalent to just "> 0"
  isCool(&)    // lets you invoke an arbitrary function

The latter is assuming something like #480 gets implemented, so f(&)/f & is shorthand for $ => f($).

If we just want to write a function condition, we need to somehow distinguish from a regular identifier. For example, [isCool] already has a meaning: array of length 1, where the element gets named isCool. Maybe there would be another notation to denote "needs to match the guard of this function". Let us know if you have ideas.

JUSTIVE commented 10 months ago

Great to hear that! I just needed the latter one (invoking a function), and the isCool(&) notation looks fine. I'm not that familiar with the civet syntax(Just tried it yesterday), so It's not very appropriate for me to suggest an idea, but the array-like syntax looks very exotic. Does Civet's design towards to point-free style? if so, how about just leave 'isCool` alone?

switch x
  isCool(&)
    "Something"
  isCool         //equivalent to above
     "Nothing"
edemaine commented 10 months ago

The trouble is that a pattern of just isCool is ambiguous. It could mean:

  1. Equality to another variable isCool in a higher scope (e.g. global). We currently write this as ^isCool.
  2. Whether isCool(x) returns true. I'm proposing writing this as isCool(&).
  3. Always match (like else), assigning a new name isCool to x. (This is potentially useful if x is a complex expression.)

You're arguing for option (2). If you'd asked me, I would have guessed that we already implemented (3), as it's what's symmetric with the current meaning of [isCool] for example. In fact, there's currently no meaning, and given the ambiguity, that might be best...?

JUSTIVE commented 2 weeks ago

Came back to this thread, about a year later. Been using the Civet lightly with joy so far, and I'm writing a post introducing the Civet in my language now, by comparing its features against alternatives (raw js, and typescript with ts-pattern).

The trouble is that a pattern of just isCool is ambiguous. It could mean:

  1. Equality to another variable isCool in a higher scope (e.g. global). We currently write this as ^isCool.
  2. Whether isCool(x) returns true. I'm proposing writing this as isCool(&).
  3. Always match (like else), assigning a new name isCool to x. (This is potentially useful if x is a complex expression.)

You're arguing for option (2). If you'd asked me, I would have guessed that we already implemented (3), as it's what's symmetric with the current meaning of [isCool] for example. In fact, there's currently no meaning, and given the ambiguity, that might be best...?

Sorry for forgetting to reply, I was asking for option 2 then, but now I see why it's difficult to do. I was naively thinking 'What if the matching symbol's type is a function, why not let it apply the value?'. and of course, just assumed that 'The function would be a predicate function'. isCool(&) and match when evaluated value is truthy/falsy seems pretty legit to me now.

back to my writings, while comparing with ts-pattern, the Civet's pattern matching also lacks some complicated guard clause, just like F# and ts-pattern. Maybe sounds familiar as old comments above, but I'm imagining something like this:

/* just an option type */
type some<T> = { __type: "some", value: T }
type none = { __type: "none" }
type option<T> = some<T> | none

const grade = 
  (score: option<number>) =>
    switch score
      { __type: "some", value } if value > 80
        "A" 
      { __type: "some", value } if value > 60
        "B"
      { __type: "some", value } if value > 40
        "C"
      { __type: "some", value } 
        "E"
      { __type: "none" }
        "F"

the point is, having some conditional expression next to the pattern to narrow down

the example might be inappropriate since the type variant is just some and none, so here's a better one.

const fruitQualifyingResult  = 
  (value: Fruits) =>
    switch value
      { __type : "Apple", weigth } if weight > 500
      { __type : "Banana", length } if length > 15
      { __type : "WaterMelon", sweetness } if sweetness > 16
        { __type : "Ok", value }
      else
        { __type : "No", value }