jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.52k stars 1.98k forks source link

Pattern matching with destructuring #1419

Closed itay closed 13 years ago

itay commented 13 years ago

Description

I noticed that this has been kind of discussed in #108, but I thought I'd bring it up again with some more examples that show why CoffeeScript is poised to make use of something like. I won't repeat the basic description from #108, and the examples should make this clear.

Examples

Here are a few examples, using a made-up syntax.

An implementation for reduce:

# more CoffeeScript like implementation
reduce = (arr, f, accumulator) ->
    match arr
        when [first, rest...] 
            return reduce(rest, f, f(first, accumulator))
        when [first] 
            return f(first, accumulator)
        when []
            return accumulator

This is the bastardization of the Option paradigm from OCaml/Scala (Maybe in Haskell):

class Option
    constructor: ->

class Some extends Option
    constructor: (@some) ->

class Error extends Option
    constructor: (@error) ->

conditional_func = (option) ->
    match option
        when { some: value }
            alert "Got some value: #{value}"
        when { error: value }
            alert "Got error: #{error}"

conditional_func(new Some(5))
conditional_func(new Error("no value present"))

And some binary tree traversal:

class Tree
    constructor: (@left, @key, @value, @right) ->

class Leaf
    constructor: (@key, @value) ->

find = (tree, needle) ->
    match tree
        when {left: left, key: key, value: value, right: right} # Tree
            if needle is key
                return value
            else if needle < key
                return find(left, needle)
            else
                return find(right, needle)
        when {key: key, value: value} # Leaf
            if key is needle
                return value
            else
                return null

And a random one:

some_func = (arr) ->
    match arr
        when [1, 2, rest...]
            alert "first"
        when [5, 10]
            alert "something else"
        else
            alert "no match"

Pros and Cons

Here are my thoughts on the arguments for and against

Pros:

  1. I think it's a clean way to deconstruct objects and do all the conditionals for you.
  2. It reads to me as very CoffeeScript-like.
  3. The Option/Some/Error structure is very useful for error handling.
  4. There isn't really even a need to introduce a new keyword. You could reuse "switch", and just make it have correct behavior if it has a deconstructing statement

    Cons:

  5. I'm not sure that my last example really works. My guess is that if you switch the order of the Tree and Leaf patterns, you'll end up with it being broken since the Leaf signature matches the Tree one as well. But that's also true if you wrote the "if" chain yourself - you have to be careful what you're checking for
  6. There's an argument to be made that the simplistic way to write patterns hides the complex conditionals behind it. But, I think the conciseness is worth it.

Anyway, I'd be happy to hear thoughts about it, or if people think it isn't worth it.

michaelficarra commented 13 years ago

This is a nicer way to write your tree traversal example, and it's valid coffeescript:

class Tree then constructor: (@left, @key, @value, @right) ->
class Leaf then constructor: (@key, @value) ->

find = (tree, needle) ->
  switch tree.constructor
    when Tree
      {left, key, value, right} = tree
      if needle is key
        value
      else if needle < key
        find left, needle
      else
        find right, needle
    when Leaf
      {key, value} = tree
      if key is needle
        return value

I love pattern matching in Haskell/Scala, and in those languages it's a killer feature. But I just don't think it works with CS/JS semantics. After seeing my alternative, do you still favour pattern matching?

itay commented 13 years ago

I think your argument/alternative boils down to the fact that because CS already has assignment deconstruction (e.g. {left, key, value, right} = tree), the added benefit of having the deconstruction happen as part of the when statement is marginal. I think this is a reasonable point of view.

Personally, I prefer my syntax, for a couple of reasons:

  1. I don't like the ".constructor" thing - it kind of scares me :)
  2. I think that there isn't as concise a way to do array deconstruction like in my reduce example. For me, this alone nearly justifies adding the feature.
  3. I think there may be cases where the "when" style syntax for pattern matching would be concise, expressive, and clear.
  4. It's easily extensible to things like regexes, for example.

That said, I realize it's mostly a stylistic issue. It's just something I always wish existed. One use case I have in mind is to reduce the need to have both success and error callbacks to HTTP requests. I'd just wrap it in Success(...) and Error(...) objects. The end result is the same, I just prefer this way of expressing it.

michaelficarra commented 13 years ago

The main problem with your proposal is that you just can't really pattern match. Are you looking for the first "pattern" in which your object hasOwnPropertys all of the keys? Here's another one of your examples done with current coffeescript:

reduce = (arr, f, accumulator) ->
  switch arr?.length
    when 0
      accumulator
    when 1
      f array[0], accumulator
    else
      reduce arr[1..], f, f arr[0], accumulator

or if you would prefer I pepper in some deconstruction...

reduce = (arr, f, accumulator) ->
  [first, rest...] = arr
  switch arr?.length
    when 0
      accumulator
    when 1
      f first, accumulator
    else
      reduce rest, f, f first, accumulator

I'd like to see a use case with your suggested style of pattern matching where the code is significantly easier to understand or the solution is significantly easier to express. Your most recently mentioned callback example is neither, since it would be better written the way I rewrote your tree traversal example. It doesn't even have to be that significant.

itay commented 13 years ago

To me, it seems that my definition of reduce is easier to understand and more natural than your solution. Here's another one with arrays where you want to iterate over a pair of values (I saw this on the Google Group, I think):

Here is the suggested code from the group:

sum_pairs = (array) ->
    sum = 0
    for i in [0...array.length - 1]
      sum += array[i].distance array[i+1]

    return sum

And here's how I'd write it with pattern matching:


sum_pairs = (array) ->
    sum = 0
    match array
        when [a, b, rest...]
            sum += (a + b + sum_pairs(array))
        else
            sum = 0

    return sum

Imagine we have a function for doing a POST on a URL along the following lines:

http_post = (url, params, success, error) ->
    # Do the POST
    response = ...

    if response.error?
        success(response)
    else
        error(response)

A caller might look like this:

get_followers = (user_id) ->
    http_post("https://twitter.com/api/following", {user_id: user_id},
        success = (response) =>
            # do something with the response
        error = (response) =>
            # do something with the error
    )

With pattern matching, I'd write it along the following lines:

http_post = (url, params, callbackr) ->
    # Do the POST
    response = ...

    result = {
        success: response.error?
        status: response.status
        response: response
        error: response.error
    }

    callback(result)

get_followers = (user_id) ->
    http_post("https://twitter.com/api/following", {user_id: user_id},
        (result) =>
            match result
                when { success: true, status: 200, response: response }
                    # do something with the response
                when { success: true, status: 201, response: response }
                    # do something else with the response
                when { success: false, error: error}
                    # do something with the error
    )

Now, I don't know which one is better. For the first example, the loop version is probably better, unless you like recursive functions. For the second, I'm not sure. I could go either way.

Like I said, I think your argument is reasonable that for the most part it's not necessary because assignment deconstruction brings us most of the way there. I still think that for the reduce example, my pattern-matching version is cleaner.

To your question about whether the notion of pattern matching on an object is just checking if hasOwnProperty is true for all the fields (and then subsequently doing any equality checks should I have given a constant, like in the status: 200 case above), then yes, that was my plan. It's not fool proof (and like I said, it is order dependent, but that's true for OCaml/Scala as well), but I think it's good enough.

accelware commented 13 years ago

Well, when { success: true, status: 201, response: response } can probably be written as when { success: true, status: 201, response }, if {a, b} is the implicit for {a:a, b:b}

Plus, the guard expression should be supported too.

itay commented 13 years ago

@accelware, you're right on both counts. I usually choose the more verbose and clearer version :)

randrew commented 13 years ago

I'd love to see this get in. It would be super awesome.

jashkenas commented 13 years ago

This has been discussed several times before -- pattern matching inspired by static languages is a particularly poor fit for JavaScript (and by extension CoffeeScript), because types are extremely weak in JS.

In addition, because if/else and switch statements are expressions in CoffeeScript, any use of match can just as well be written in terms of them, but more clearly and powerfully, with arbitrary predicates.