{"id":23068125,"url":"https://github.com/mailund/pmatch","last_synced_at":"2025-06-17T09:36:10.126Z","repository":{"id":72429778,"uuid":"119651488","full_name":"mailund/pmatch","owner":"mailund","description":"Pattern matching DSL for R","archived":false,"fork":false,"pushed_at":"2020-01-31T05:25:24.000Z","size":1378,"stargazers_count":21,"open_issues_count":7,"forks_count":1,"subscribers_count":5,"default_branch":"master","last_synced_at":"2023-10-20T21:29:22.333Z","etag":null,"topics":["dsl","meta-programming","pattern-matching","r"],"latest_commit_sha":null,"homepage":null,"language":"R","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mailund.png","metadata":{"files":{"readme":"README.Rmd","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2018-01-31T07:33:06.000Z","updated_at":"2022-07-28T09:30:20.000Z","dependencies_parsed_at":null,"dependency_job_id":"e4e67c61-d7f2-4693-9c18-c5579c8a3bab","html_url":"https://github.com/mailund/pmatch","commit_stats":null,"previous_names":[],"tags_count":3,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mailund%2Fpmatch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mailund%2Fpmatch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mailund%2Fpmatch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mailund%2Fpmatch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mailund","download_url":"https://codeload.github.com/mailund/pmatch/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":229911625,"owners_count":18143342,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsl","meta-programming","pattern-matching","r"],"created_at":"2024-12-16T05:20:42.255Z","updated_at":"2024-12-16T05:20:42.829Z","avatar_url":"https://github.com/mailund.png","language":"R","readme":"---\noutput: github_document\n---\n \n\u003c!-- README.md is generated from README.Rmd. Please edit that file --\u003e\n\n```{r, echo = FALSE}\nknitr::opts_chunk$set(\n  collapse = TRUE,\n  comment = \"#\u003e\",\n  fig.path = \"README-\"\n)\n\nsuppressPackageStartupMessages(library(magrittr, quietly = TRUE))\nsuppressPackageStartupMessages(library(pmatch, quietly = TRUE))\n\n```\n\n# Haskell-like pattern matching in R\n\n[![Licence](https://img.shields.io/badge/licence-GPL--3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html)\n[![lifecycle](http://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://www.tidyverse.org/lifecycle/#experimental)\n[![Project Status: Active](http://www.repostatus.org/badges/latest/active.svg)](http://www.repostatus.org/#active)\n[![Last-changedate](https://img.shields.io/badge/last%20change-`r gsub('-', '--', Sys.Date())`-green.svg)](/commits/master)\n[![packageversion](https://img.shields.io/badge/Package%20version-0.1.5.9000-orange.svg?style=flat-square)](commits/master)\n[![Travis-CI Build Status](http://travis-ci.org/mailund/pmatch.svg?branch=master)](https://travis-ci.org/mailund/pmatch)\n[![AppVeyor Build Status](http://ci.appveyor.com/api/projects/status/wvyqe7bfp4a2rm77?svg=true)](https://ci.appveyor.com/project/mailund/pmatch)\n[![Coverage Status](http://img.shields.io/codecov/c/github/mailund/pmatch/master.svg)](https://codecov.io/github/mailund/pmatch?branch=master)\n[![Coverage Status](http://coveralls.io/repos/github/mailund/pmatch/badge.svg?branch=master)](https://coveralls.io/github/mailund/pmatch?branch=master)\n[![CRAN status](http://www.r-pkg.org/badges/version/pmatch)](https://cran.r-project.org/package=pmatch)\n[![CRAN downloads](http://cranlogs.r-pkg.org/badges/grand-total/pmatch)](https://cran.r-project.org/package=pmatch)\n[![minimal R version](https://img.shields.io/badge/R-%E2%89%A53.3-blue.svg)](https://cran.r-project.org/)\n\n\nThe goal of the `pmatch` package is to provide structure pattern matching, similar to Haskell and ML, to R programmers. The package provide functionality for defining new types and for matching against the structure of such types.\n\nThe idea behind pattern matching is that we define types by how we create them, and we have ways of matching a pattern of constructors against a value to pick the one that matches the value.\n\nThe simplest example is a type defined just from constants. For example, we can define the type `enum` to consist of one of `ONE`, `TWO`, or `THREE`.\n\n```{r}\nenum := ONE | TWO | THREE\n```\n\nAny of these three constants will be created by this command. If you print them, they just give you their names:\n\n```{r}\nONE\nTWO\nTHREE\n```\n\nThe interesting feature is that we can match against these constructor-constants. Using the `case_func` function we can declare functions where we can pick a pattern that matches a value.\n\n```{r}\nelements \u003c- list(ONE, TWO, THREE)\nmatch \u003c- case_func(\n    ONE -\u003e 1,\n    TWO -\u003e 2,\n    THREE -\u003e 3\n)\nfor (elm in elements) {\n    value \u003c- match(elm)\n    cat(\"Element\", toString(elm), \"maps to value\", toString(value), \"\\n\")\n}\n```\n\nThe `case_func` function works by matching its first element--which should be a value constructed as a type we have defined with `:=`--against a list of patterns. The pattern arguments are on the form `pattern -\u003e expression`. The value is matched against the patterns in turn and the first pattern that matches will be chosen. The expression to the right of the arrow is then evaluated and the result is returned.\n\nThe patterns do not need to be literal constants. You can also use variables. These will be bound to the matching value and the expression that is evaluated will see such variables bound.\n\n```{r}\nelements \u003c- list(ONE, TWO, THREE)\nmatch \u003c- case_func(\n    ONE -\u003e 1,\n    v -\u003e v\n)\nfor (elm in elements) {\n    value \u003c- match(elm)\n    cat(\"Element\", toString(elm), \"maps to value\", toString(value), \"\\n\")\n}\n```\n\nIt gets more interesting when we move beyond constants. The `:=` operator also allows you to define function-constructors. These are written simply as you would write a function call, but the variables are interpreted as parameters of the constructor. For example, we could define:\n\n```{r}\nzero_one_two_three := ZERO | ONE(x) | TWO(x,y) | THREE(x,y,z)\n```\n\nThe first constructor, `ZERO`, is just a constant as before, but the other three takes arguments.\n\n```{r}\nONE(1)\nTWO(1,2)\nTHREE(1,2,3)\n```\n\nWhen we use `case_func` to match against such patterns, we can bind variables to the values they contain.\n\n```{r}\nf \u003c- case_func(\n    ZERO         -\u003e 0,\n    ONE(x)       -\u003e x,\n    TWO(x,y)     -\u003e x + y,\n    THREE(x,y,z) -\u003e x + y + z\n)\n\nf(ZERO)\nf(ONE(1))\nf(TWO(1,2))\nf(THREE(1,2,3))\n```\n\nYou can nest these patterns to match on more complex values\n\n```{r}\nf \u003c- case_func(\n    ZERO               -\u003e 0,\n    ONE(x)             -\u003e x,\n    TWO(ONE(x),ONE(y)) -\u003e x + y + 42,\n    TWO(x,y)           -\u003e x + y,\n    THREE(x,y,z)       -\u003e x + y + z\n)\n\nf(TWO(ONE(10),ONE(-10)))\n```\n\nYou have to be careful with the order of expressions, though. If we flipped the two `TWO` patterns, the first one, `TWO(x,y)` would match first and we would be trying to add together `ONE(10)` and `ONE(-10)`, which would result in an error since we do not have an addition operator defined on these types.\n\nA variable will match any pattern, so you can use one as a default case. I prefer to use the `.` variable.\n\n```{r}\nh \u003c- case_func(\n    1 -\u003e 1, 13 -\u003e 13, . -\u003e 24\n)\nh(42)\n```\n\nWhen you define function constructors, you can give the arguments types. You do this by adding `:` and a type name to the argument. For example, we could define\n\n```{r}\none_or_two := ONE(x : numeric) | TWO(x : numeric, y : numeric)\n```\n\nWe would now get an error if the arguments we provide to the constructors were not `numeric`:\n\n```{r, error=TRUE}\nONE(1)\nONE(\"foo\")\n```\n\nConstructors and pattern matching becomes even more powerful when you start to define recursive data structures. You can, for example, define a binary tree like this:\n\n```{r}\ntree := L(elm : numeric) | T(left : tree, right : tree)\n```\n\nYou can then write a very succinct depth first traversal that collects the leaves of such a tree like this:\n\n```{r}\nf \u003c- case_func(\n    L(v) -\u003e v, \n    T(left,right) -\u003e c(f(left), f(right))\n)\nx \u003c- T(T(L(1),L(2)), T(T(L(3),L(4)),L(5)))\nf(x)\n```\n\n### Binding local variables\n\nYou can also assign to variables in the current namespace using subscripting on the special object `bind`:\n\n```{r}\nbind[a,b] \u003c- 1:2\na\nb\n```\n\nWith the `bind` object, you can match against patterns, as with the `case_func` function, and matched variables will be added to the namespace where you invoke subscripting on `bind`:\n\n```{r}\nx\nbind[T(left, right)] \u003c- x\nleft\nright\n```\n\n### Testing more than one pattern\n\nIf you want to test more than one pattern at a time you can create a data type for tuples, e.g.\n\n```{r}\ntuples := ..(first, second) | ...(first, second, third)\nf \u003c- case_func(..(.,.) -\u003e 2, ...(.,.,.) -\u003e 3)\nf(..(1, 2))\nf(...(1, 2, 3))\n```\n\n\n\nFor more examples, see below.\n\n## Installation\n\nYou can install the stable version of pmatch from CRAN using\n\n```{r gh-installation-cran, eval = FALSE}\ninstall.packages(\"pmatch\")\n```\n\n\nYou can install the development version pmatch from github with:\n\n```{r gh-installation-github, eval = FALSE}\n# install.packages(\"devtools\")\ndevtools::install_github(\"mailund/pmatch\")\n```\n\n## Examples\n\nTo show how the `pmatch` package can be used, I will use three data structures that I have implemented without `pmatch` in my book on [*Functional Data Structures in R*](http://amzn.to/2Eb4RKK): linked lists, plain search trees, and red-black search trees. \n\nTo run the examples below, you will need to use the `magrittr` package for the pipe operator, `%\u003e%`.\n\n```r\nlibrary(magrittr)\n```\n\n### Linked lists\n\nThe `list` type in R is allocated to have a certain size when it is created, and changing the size of `list` objects involve creating a new object and moving all the elements from the old object to the new. This is a linear time operation, so growing lists usually lead to quadratic running times. With linked lists, on the other hand, you can prepend elements in constant time--at the cost of linear time random access.\n\nYou can implement a linked list using `list` objects. You simply construct a list that contains two elements, the head of the linked lists--traditionally called `car`--and the tail of the list--another linked list, traditionally named `cdr`. You need a special representation for empty lists, and a natural choice is `NULL`. With `pmatch` we will use a constant instead, though, so we can pattern match on empty lists.\n\nWe can define a linked list using the `pmatch` syntax like this:\n\n\n```{r list-definition}\nlinked_list := NIL | CONS(car, cdr : linked_list)\nlst \u003c- CONS(1, CONS(2, CONS(3, NIL)))\n```\n\nAlthough R doesn't implement tail recursion optimization, habit forces me to write tail recursive functions. For list functions, this usually means providing an accumulator parameter. Other than that, recursive functions operating on linked lists should simply match on `NIL` and `CONS` patterns. Two examples could be computing the length of a list and reversing a list:\n\n```{r list-functions}\nlist_length \u003c- case_func(acc = 0,\n        NIL -\u003e acc,\n        CONS(car, cdr) -\u003e list_length(cdr, acc + 1)\n)\n\nlist_length(lst)\n\nreverse_list \u003c- case_func(acc = NIL,\n        NIL -\u003e acc,\n        CONS(car, cdr) -\u003e reverse_list(cdr, CONS(car, acc))\n)\n\nreverse_list(lst)\n```\n\nTranslating to and from vectors/`list` objects is relatively simple. To go from a vector to a linked list, we use `NIL` and `CONS`, and to go the other direction we use pattern matching:\n\n```{r}\nvector_to_list \u003c- function(vec) purrr::reduce_right(vec, ~ CONS(.y, .x), .init = NIL)\n\nlist_to_vector \u003c- function(lst) {\n  n \u003c- list_length(lst)\n  v \u003c- vector(\"list\", length = n)\n  f \u003c- case_func(i,\n          NIL -\u003e NULL,\n          CONS(car, cdr) -\u003e {\n            v[[i]] \u003c\u003c- car\n            f(cdr, i + 1)\n            }\n  )\n  f(lst, 1)\n  v %\u003e% unlist\n}\n\nlst \u003c- vector_to_list(1:5)\nlist_length(lst)\nlist_to_vector(lst)\nlst %\u003e% reverse_list %\u003e% list_to_vector\n```\n\n### Search trees\n\nSearch trees are binary trees that holds values in all inner nodes and satisfy the invariant that all values in a left subtree are smaller than the value in an inner node, and all values in the right subtree are larger.\n\nWe can define a search tree like this:\n\n```{r}\nsearch_tree := E | T(left : search_tree, value, right : search_tree)\n```\n\nHere, we use an empty tree, `E`, for leaves. We only store values in inner nodes, created with the constructor `T`.\n\n```{r}\ntree \u003c- T(T(E,1,E), 3, T(E,4,E))\ntree\n```\n\nBecause of the invariant, we know where values should be found if they are in a tree. We can look at the value in the root of a subtree. If it is larger than the value we are searching for, we need to search to the left. If it is smaller, we need to search to the right. Otherwise, it must be equal to the value. If we reach an empty tree in this search, then we know the value is no the tree.\n\n```{r}\nmember \u003c- case_func(x,\n        E -\u003e FALSE,\n        T(left, val, right) -\u003e {\n          if (x \u003c val) member(left, x)\n          else if (x \u003e val) member(right, x)\n          else TRUE\n        }\n)\nmember(tree, 0)\nmember(tree, 1)\nmember(tree, 2)\nmember(tree, 3)\nmember(tree, 4)\n```\n\nSince data in R, in general, are immutable, we cannot update search trees. We can, however, create copies with updated structure, and because R implements \"copy-on-write\", this is an efficient way of updating the structure of data we work on. If we insert elements into a search tree, what we will really be doing is to create a new tree that holds all the values the old tree held plus the new values. If the value is already in the old tree we do not add it again, but we will be returning a new tree. We create the new tree in a recursion. Whenever we call recursively, we create a new inner node that will contain one subtree that is an exact copy of one of the subtrees from the old tree--shared with the old tree so no actual copying takes place--and one subtree that is created in the recursive insertion. The recursion goes left or right using the same logic as in the `member` function. If we find that the element is already in the tree, we terminate the recursion with the tree that contains the value. If we reach an empty tree, the element was not in the old tree, but we have found the place where it should be in the new tree, so we create an inner tree with two empty subtrees and the value.\n\n```{r}\ninsert \u003c- case_func(x,\n        E -\u003e T(E, x, E),\n        T(left, val, right) -\u003e\n          if (x \u003c val)\n            T(insert(left, x), val, right)\n          else if (x \u003e val)\n            T(left, val, insert(right, x))\n          else\n            T(left, x, right)\n)\n\ntree \u003c- E\nfor (i in sample(2:4))\n  tree \u003c- insert(tree, i)\n\nfor (i in 1:6) {\n  cat(i, \" : \", member(tree, i), \"\\n\")\n}\n```\n\n### Red-black search trees\n\nThe worst-case time usage for both of these functions is proportional to the depth of the tree, and that can be linear in the number of elements stored in the tree. If we keep the tree balanced, though, the time is reduced to logarithmic in the size of the tree. A classical data structure for keeping search trees balanced is so-called *red-black* search trees. Implementing these using pointer or reference manipulation in languages such as C/C++ or Java can be quite challenging, but in a functional language, balancing such trees is a simple matter of transforming trees based on local structure.\n\nRed-black search trees are binary search trees where each tree has a colour associated, either red or black. We can define colours using constant constructors and define a red-black search tree by extending the plain search tree:\n\n```{r}\ncolour := R | B\nrb_tree := E | T(col : colour, left : rb_tree, value, right : rb_tree)\n```\n\nExcept for including the colour in the pattern matching, the `member` function for this data structure is the same as for the plain search tree.\n\n```{r}\nmember \u003c- case_func(x,\n        E -\u003e FALSE,\n        T(col, left, val, right) -\u003e {\n          if (x \u003c val) member(left, x)\n          else if (x \u003e val) member(right, x)\n          else TRUE\n        }\n)\n\ntree \u003c- T(R, E, 2, T(B, E, 5, E))\nfor (i in 1:6) {\n  cat(i, \" : \", member(tree, i), \"\\n\")\n}\n```\n\nRed-black search trees are kept balanced because we enforce these two invariants:\n\n1. No red node has a red parent.\n2. Every path from the root to a leaf has the same number of black nodes.\n\nIf every path from root to a leaf has the same number of black nodes, then the tree is perfectly balanced if we ignored the red nodes. Since no red node has a red parent, the longest path, when red nodes are considered, can be no longer than twice the length of the shortest path.\n\nThese invariants can be guaranteed by always inserting new values in red leaves, potentially invalidating the first invariant, and then rebalancing all sub-trees that invalidate this invariant, and at the end setting the root to be black. The rebalancing is done when returning from the recursive insertion calls that otherwise work as insertion in the plain search tree.\n\n```{r}\ninsert_rec \u003c- case_func(x,\n        E -\u003e T(R, E, x, E),\n        T(col, left, val, right) -\u003e {\n          if (x \u003c val)\n            balance(T(col, insert_rec(left, x), val, right))\n          else if (x \u003e val)\n            balance(T(col, left, val, insert_rec(right, x)))\n          else\n            T(col, left, x, right) # already here\n        }\n)\ninsert \u003c- function(tree, x) {\n  tree \u003c- insert_rec(tree, x)\n  tree$col \u003c- B\n  tree\n}\n```\n\nThe transformation rules for the `balance` function are shown in the figure below:\n\n![](http://users-cs.au.dk/mailund/RBT-transformations.png)\n\nEvery time we see one of the trees around the edges, we must transform it into the tree in the middle. We can implement these transformations as simple as this:\n\n```{r}\nbalance \u003c- function(tree) {\n  cases(tree,\n        T(B,T(R,a,x,T(R,b,y,c)),z,d) -\u003e T(R,T(B,a,x,b),y,T(B,c,z,d)),\n        T(B,T(R,T(R,a,x,b),y,c),z,d) -\u003e T(R,T(B,a,x,b),y,T(B,c,z,d)),\n        T(B,a,x,T(R,b,y,T(R,c,z,d))) -\u003e T(R,T(B,a,x,b),y,T(B,c,z,d)),\n        T(B,a,x,T(R,T(R,b,y,c),z,d)) -\u003e T(R,T(B,a,x,b),y,T(B,c,z,d)),\n        otherwise -\u003e tree)\n}\n```\n\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmailund%2Fpmatch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmailund%2Fpmatch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmailund%2Fpmatch/lists"}