nim-lang / RFCs

A repository for your Nim proposals.
136 stars 23 forks source link

caseStmtMacros to support C#8-like Recursive Patterns matching (Deconstruction Pattern) #57

Open timotheecour opened 6 years ago

timotheecour commented 6 years ago

see https://gist.github.com/Araq/169d1e24b2b996d024a780ef6a4e6c09#file-casestmtmacros-nim for context, introduced by @Araq for a 1st version of this concept.

IT reminds me of Recursive Patterns matching (Deconstruction Pattern) in C#8, see https://www.infoq.com/articles/cs8-ranges-and-recursive-patterns

switch (employee) {
 case Employee {Name: "Bassam Alugili", Company: Company(_, _, _)} employeeTmp: ...
 case Employee(_, _, ("Stratec", _, _)) employeeTmp: ...
  case Employee(_, _, Company(_, _, _)) employeeTmp: ...
}

where?

matchers.nim

links

snippet copied over from @araq's post

{.experimental: "caseStmtMacros".}

import macros

macro match(n: tuple): untyped =
  result = newTree(nnkIfStmt)
  let selector = n[0]
  for i in 1 ..< n.len:
    let it = n[i]
    case it.kind
    of nnkElse, nnkElifBranch, nnkElifExpr, nnkElseExpr:
      result.add it
    of nnkOfBranch:
      for j in 0..it.len-2:
        let cond = newCall("==", selector, it[j])
        result.add newTree(nnkElifBranch, cond, it[^1])
    else:
      error "'match' cannot handle this node", it
  echo repr result

case ("foo", 78)
of ("foo", 78): echo "yes"
of ("bar", 88): echo "no"
else: discard
GULPF commented 6 years ago

I think it's hard to support if. How would the compiler know that a macro should be invoked? For case it's simple since the type of the expression decides it. Could you give an example of how ifStmtMacros would work?

mratsim commented 6 years ago

In a v2 I'd like to have assignment support when deconstructing, maybe via a special prefix symbol like @:

case ("foo", 78)
of ("foo", @a): echo $(a*2) # prints 78 * 2 = 156
of ("bar", 88): echo "no"
else: discard
timotheecour commented 6 years ago

@GULPF @araq on second thought, ifStmtMacros are not needed, maybe we can just use a regular macro (which itself could share code with macro match perhaps, idk):

let expr = ("foo", 78)
if expr.matches(("foo", _)): discard # matches a tuple of length 2
if expr.matches(("foo", )): discard # matches a tuple of length 1
if expr.matches("foo"): discard # same as if expr == "foo"
# works with matching against derived type, allowing also to capture derived instance
if expr.matches(Employee(name: "bob"), myderivedinstance): echo myderivedinstance.name # same as C#  example `if (bassam is Employee {Name: "bob"} myderivedinstance)`

this is not specific to if and could be used in any context where a boolean is expected, eg:

when expr.matches(("foo", _)) : discard
doAssert expr.matches(("foo", _))
let ok = expr.matches(("foo", _))

implementation

macro matches(pattern: untyped) = untyped # will evaluate to bool
macro matches(pattern: untyped, capture: untyped) = untyped # will evaluate to bool and capture derived pattern in `capture`, as in C# and example above
drslump commented 6 years ago

IMHO these proposals should start by providing a good example of current Nim code that would benefit from it. Is there something that gets written time and time again that would improve with this? (also including readability not only terseness when writing the code).

I do think that deconstructing is a good tool though, but often times we just want to guard and alias to locals, which is an use case that can be solved more cleanly with something like this I think:

if let a = ("foo", a):
   echo $(a*2)

if let _ = ("foo", _):   # `_` is just used to force deconstructuring
  echo "hi!"

# from the original comment
# case let a, b = ("foo", 78)
# of ("foo", a): echo $(a*2)  # referencing `b` would produce an error, not in scope
# of ("bar", c): echo "no"  # error: `c` is not part of the case let statement 
# else: discard

# this I think is better
case ("foo", 78)
of let a = ("foo", a): echo $(a*2)
of let _ = ("foo", _): echo "ok"  # forced matching
of ("bar", 80): echo "no"  # no destructuring or matching
else: discard

Alternatively, a fully featured match expression could be nice but I think that would change how the language is used quite substantially.

LemonBoy commented 6 years ago

For reference also scheme's matchable is implemented as a macro and is quite flexible.

mratsim commented 6 years ago

Seems like Rust-like if let, while let, case let for deconstruction + assignment is coming back quite often (and this fits a similar needs to the new Python := I feel).

The most ergonomic pattern matching I came across was Haskell there are 2 syntaxes, unfortunately the nicer one does not fit Nim at all (though the second one is very similar to Nim's).

Syntax 1:

take  0   _       =  []
take  _   []      =  []
take  n   (x:xs)  =  x : take (n-1) xs

Syntax 2

take m ys = case (m,ys) of
              (0,_)    ->  []
              (_,[])   ->  []
              (n,x:xs) ->  x : take (n-1) xs
timotheecour commented 6 years ago

isn't this {.experimental: "caseStmtMacros".} gonna run into https://github.com/nim-lang/Nim/issues/8676#issuecomment-413943400 ? @yglukhov does your proposal https://github.com/nim-lang/Nim/issues/8676#issuecomment-414665222 address this for caseStmtMacros?

alehander92 commented 6 years ago

I like the https://github.com/nim-lang/Nim/issues/8649#issuecomment-413323627 idea, if one can capture a submatch

Pattern matching is a big ergonomic win, there are so many if a.field == and a.field.b == and a.other == which will look so much better with a matching syntax

@krux02 's macro in his ast pattern lib can also be made to work on runtime values, we just have to agree on some kind of standard API

krux02 commented 6 years ago

With the features that are already there in Nim I implemented recursive pattern matching fo AST nodes here: https://github.com/krux02/ast-pattern-matching.

Please get to the point and explain why the given language features are not enough. The issue is just a compilation of random ideas and in general quite a mess.

andreaferretti commented 6 years ago

Whatever comes out of this, I would argue that it should be enough to support both patty and AST pattern matching (both suitably adapted)

alehander92 commented 6 years ago

The problem is that both @andreaferretti 's patty and @krux02 's ast-pattern-matching currently lack a lot of features, but in a very different way: e.g.

now, I really want to have a working lib:

on the other hand the patty and the ast-pattern-matching dsl-s differ .. but not greatly. this means we might have two slightly different dsl-s for the same thing in an already small ecosystem.

Do you guys want to compromise on a single api and somehow combine the two libs approaches into a single , more universal nim pattern matching lib?

Such a lib can still be

I worked before on personal forks of both libs, but I couldn't decide which would be more feasible to adapt (and I didn't have time). I also planned to make my own match macro, but then we will just have 3 libs (insert xkcd standards comic)

andreaferretti commented 6 years ago

I would certainly want a library that generalize both patty and @krux02s' library. I am also not especially attached to patty: if a better library comes out that does pattern matching, I will be happy yo deprecate it. It was just a small experiment, but it is by no means a complete solution.

alehander92 commented 6 years ago

so I decided to test my ideas in https://github.com/alehander42/gara

@andreaferretti I got the idea for many features from patty , especially unification, I am thinking of a more powerful variant of it: e.g. support @[@x, expression(@x)] (to check expressions based on captured names), do you think this would be useful?

@timotheecour I also implemented your matches idea, but I am not sure about the API: I have a.matches(pattern) which returns bool and a.maybeMatches(pattern) which returns an option with a tuple[name: value..] with the captured names(see more details in README). I feel that's cleaner than passing a capture arg?

metagn commented 3 years ago

Fusion pattern matching does not seem to have "custom matching" or "deconstruction", but it seems plenty sufficient for most pattern matching (Some(@x) => (isSome: true, get: @x)). If not, assigns allows for overloading unpacks based on type (matches are an abstraction over unpacks, so not as efficient), but I don't like to advertise this library because fusion matching is a lot more comprehensive and will be more stable.

konsumlamm commented 3 years ago

Fusion pattern matching does not seem to have "custom matching" or "deconstruction", but it seems plenty sufficient for most pattern matching.

Agreed, I think this RFC has been superseded by #245. fusion/matching implements all of the originally proposed features (recursive matching, _ placeholder and matching in if via the matches macro).

For "custom matching" or "deconstruction", most mechanisms can be customized (write procs to support object patterns, write an items iterator to support seq patterns, write a kind proc to support variant patterns, etc.)

See also https://github.com/nim-lang/fusion/pull/61.