Skip to content

Instantly share code, notes, and snippets.

@mjskay
Created March 18, 2023 21:36
Show Gist options
  • Select an option

  • Save mjskay/3fe4af293e3b141eff19f66245bfd749 to your computer and use it in GitHub Desktop.

Select an option

Save mjskay/3fe4af293e3b141eff19f66245bfd749 to your computer and use it in GitHub Desktop.

Simple example of pattern matching and rewriting of expressions in R with a bquote()-style syntax.

N.B. doesn't support splicing-style syntax (..()), but shouldn't be too hard to add.

match_exprs = function(expr, ...) {
  patterns = list(...)
  for (pattern in patterns) {
    tryCatch({
      match = match_expr(expr, pattern[[2]])
      return(eval(bquote(bquote(.(pattern[[3]]))), match))
    },
      no_match = function(e) NULL
    )
  }
  stop(no_match("No pattern matches ", deparse1(expr)))
}

match_expr = function(expr, pattern) {
  if (is_placeholder(pattern)){
    return(setNames(list(expr), placeholder_name(pattern)))
  } else if (
      is.name(expr) && 
      is.name(pattern) &&
      expr == pattern
    ) {
    return(list())
  } else if (
    is.atomic(expr) &&
    is.atomic(pattern) &&
    expr == pattern
  ) {
    return(list())
  } else if (
      is.call(expr) &&
      is.call(pattern) && 
      length(expr) == length(pattern)
    ) {
    matches = .mapply(match_expr, list(as.list(expr), as.list(pattern)), NULL)
    return(do.call(c, matches))
  } else {
    stop(no_match("No pattern matches ", deparse1(expr)))
  }
}

no_match = function(...) {
  errorCondition(paste0(...), class = "no_match")
}

is_placeholder = function(expr) {
  is.call(expr) && expr[[1]] == "." && length(expr) == 2 && is.name(expr[[2]])
}

placeholder_name = function(expr) {
  deparse1(expr[[2]])
}

match_exprs(quote(f(1, z + 7)),
  g(x, y) ~ foo,
  f(1, y) ~ bar,
  f(.(x), .(y)) ~ .(x) + .(y)
)
## 1 + (z + 7)
@TimTeaFan
Copy link

TimTeaFan commented Mar 18, 2023

This is a great idea. Not sure I have completely wrapped my head around it yet. First I thought we were just going to match something, but this function alters the expression pretty much in the same way we replace strings with str_replace so we could also call it replace_exprs.

@mjskay
Copy link
Author

mjskay commented Mar 18, 2023

I see the analogy, but I think it's a bit different from replace --- replace takes a string, finds n matches in it, and replaces each with the replacement; whereas match_exprs takes a single expression and finds a single pattern (from a set) that matches the entire expression, then returns an entirely new (single) expression. So I'm not sure it's a replace operation per se.

The inspiration here is pattern matching from languages like OCaml (see the OCaml example under Tree Patterns here), hence the name. Though since R already has a match() function, another name might be more appropriate.

I am thinking the implementation also might not return an expression necessarily, but rather an arbitrary result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment