{"id":32203380,"url":"https://github.com/cfhammill/lenses","last_synced_at":"2026-02-21T12:01:51.223Z","repository":{"id":56936976,"uuid":"149823834","full_name":"cfhammill/lenses","owner":"cfhammill","description":"Elegant Data Manipulation with Lenses","archived":false,"fork":false,"pushed_at":"2019-07-10T16:13:42.000Z","size":203,"stargazers_count":27,"open_issues_count":18,"forks_count":4,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-12-09T21:40:58.537Z","etag":null,"topics":["functional-programming","r"],"latest_commit_sha":null,"homepage":"https://cfhammill.github.io/lenses","language":"R","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cfhammill.png","metadata":{"files":{"readme":"README.Rmd","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-09-21T22:15:05.000Z","updated_at":"2023-10-25T15:46:10.000Z","dependencies_parsed_at":"2022-08-21T06:50:08.144Z","dependency_job_id":null,"html_url":"https://github.com/cfhammill/lenses","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/cfhammill/lenses","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cfhammill%2Flenses","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cfhammill%2Flenses/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cfhammill%2Flenses/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cfhammill%2Flenses/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cfhammill","download_url":"https://codeload.github.com/cfhammill/lenses/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cfhammill%2Flenses/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29680147,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T11:29:27.227Z","status":"ssl_error","status_checked_at":"2026-02-21T11:29:20.292Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["functional-programming","r"],"created_at":"2025-10-22T04:38:20.701Z","updated_at":"2026-02-21T12:01:51.217Z","avatar_url":"https://github.com/cfhammill.png","language":"R","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\noutput: github_document\n---\n\n```{r setup, include = FALSE}\nknitr::opts_chunk$set(\n  collapse = TRUE,\n  comment = \"#\u003e\",\n  fig.path = \"man/figures/README-\",\n  out.width = \"100%\"\n)\n```\n\n```{r, include = FALSE}\nlibrary(dplyr)\n```\n\n# Elegant Data Manipulation with Lenses\n\n## Installation\n\n```{r, eval = FALSE}\ninstall.packages(\"lenses\")\n```\n\nor\n\n```{r, eval = FALSE}\ndevtools::install_github(\"cfhammill/lenses\")\n```\n\n## Intro\n\nWhen programming in R there are two fundamental operations we perform on our data.\nWe `view` some piece of the data, or we `set` some piece of the data to a particular\nvalue. These two operations are so fundamental that R comes with many pairs \nof `view` and `set` functions. A classic example would be `names`. Names can be \nviewed `names(x)` and set `names(x) \u003c- new_names`. Lenses are an extension\nof the idea of `view`/`set` pairs, offering the following advantages:\n\n- lenses are bidirectional (you can `set` anything you can `view`)\n- they are composable (they can be put together to `set`/`view` nested data)\n- they reduce code duplication\n- they play well with piping - [see R4DS to learn more about piping](http://r4ds.had.co.nz/pipes.html)\n- and they are non-destructive (lenses won't update your data without your permission)\n\nIn this document, we'll see a few common data manipulation operations and\nhow they can be improved with lenses.\n\n## Simple manipulations\n\nLet's take the iris data set for example, we want to perform\nsome manipulations on it.\n\n```{r}\nhead(iris)\n```\n\nWe're curious about the value of the 3rd element of the `Sepal.Length` column.\nUsing base R we can `view` it with: \n\n```{r}\niris$Sepal.Length[3]\n```\n\nwe can update (set) the value by assigning into it:\n\n```{r}\niris$Sepal.Length[3] \u003c- 100\nhead(iris, 3)\n```\n\nand we can perform some operation to update it:\n\n```{r}\niris$Sepal.Length[3] \u003c- log(iris$Sepal.Length[3])\n```\n\nThis works well, however, there are some problems. \n\nThe first problem comes with having our  `view` and `set` functions separate. \nComposing our operations isn't easy, particularly when using pipes:\n\n```{r}\niris %\u003e%\n  .$Sepal.Length %\u003e%\n  `[\u003c-`(3, 20)\n```\n\nWhoops, that's not what we wanted. Here we see `Sepal.Length` with\nthe third element replaced, but where did the rest of `iris` go! So\nwe lose information when we pipe from a `view` to a `set`. \n\nR's `set`/`view` pairs also can't be composed with function \ncompostion:\n\n```{r}\n`[\u003c-`(`$`(iris, `Sepal.Length`), 3, 20)\n```\n\nstill not what we want. It has the same problem above.\n\nThis is a failure of \"bidirectionality\", once you've chosen to use a \n`view` function, or a `set` function, you are locked into that direction.\n\nLack of composability and bidirectionality means that you frequently have\nto duplicate your code. For example, if you want to apply an operation to the\nthird element of \"Sepal.Length\", you need to specify the chain of accessors\ntwice, once in `view` mode, and once in `set` mode, making your code messy \nand cumbersome:\n\n```{r}\niris$Sepal.Length[3] \u003c- iris$Sepal.Length[3] * 2\nhead(iris, 3)\n```\n\nWe can fix both of these problems by using lenses.\n\n## Using lenses\n\nLenses give you all the power of R's `view` and `set` functions plus\nthe advantages noted above. Especially important are the composition\nand bidirectionality features. Each lens can be used with the\n[`view`](https://cfhammill.github.io/lenses/reference/view.html),\nand [`set`](https://cfhammill.github.io/lenses/reference/set.html)\nfunctions.\n\nLet's revisit the operations we performed above using lenses. \n\nThe first thing we will do is construct a lens into the \nthird element of the `Sepal.Length` component of a structure:\n\n```{r}\nlibrary(lenses)\n\nsepal_length3 \u003c- index(\"Sepal.Length\") %.% index(3)\n```\n\nIn the above code we're creating two lenses, one\ninto `Sepal.Length` and another into element 3, using the \n[`index`](https://cfhammill.github.io/lenses/reference/set.html) \nfunction. We're then composing these two lenses with\n[`%.%`](https://cfhammill.github.io/lenses/reference/lens-compose.html)\nproducing a new lens into our element of interest.\n\nNote that this lens has no idea we're going to apply it\nto `iris`. Lenses are constructed without knowing what data they \nwill be applied to.\n\nNow that we have a lens into the third element of `Sepal.Length`, \nwe can examine the appropriate element of the `iris` dataset with the \n[`view`](https://cfhammill.github.io/lenses/reference/view.html) function:\n\n```{r}\niris %\u003e% view(sepal_length3)\n```\n\nWe can update this element with the \n[`set`](https://cfhammill.github.io/lenses/reference/set.html) function:\n\n```{r}\niris %\u003e% set(sepal_length3, 50) %\u003e% head(3)\n```\n\nAnd we can apply a function to change the data. To\ndo this we can apply a function \n[`over`](https://cfhammill.github.io/lenses/reference/over.html) \nthe lens:\n\n```{r}\niris %\u003e% over(sepal_length3, log) %\u003e% head(3)\n```\n\nNote that we never had to respecify what subpart\nwe wanted, the lens kept track for us. We saw that\nthe same lens can be used to both \n[`view`](https://cfhammill.github.io/lenses/reference/view.html) and \n[`set`](https://cfhammill.github.io/lenses/reference/set.html),\nand that they can be composed easily with \n[`%.%`](https://cfhammill.github.io/lenses/reference/lens-compose.html).\n\n## More interesting lenses\n\nNow you have seen the main lens verbs and operations\n\n1. [`view`](https://cfhammill.github.io/lenses/reference/view.html): \n  see the subpart of an object a lens is focussed on.\n1. [`set`](https://cfhammill.github.io/lenses/reference/set.html): \n  set the subpart to a particular value, then return the\n  whole object with the subpart updated.\n1. [`over`](https://cfhammill.github.io/lenses/reference/over.html): \n  apply a function to the subpart, then return the \n  whole object with the subpart updated.\n1. [`%.%`](https://cfhammill.github.io/lenses/reference/lens-compose.html): \n  compose two lenses to focus on a subpart of a subpart.\n\nNow if all lenses had to offer was more composable indexing of vectors, \nyou  might not be interested in integrating them into your workflows. \nBut lenses can do a lot more than just pick and set elements in vectors. \n\nFor example, this package provides lens-ified version of `dplyr::select`.\nUnlike `select`, \n[`select_l`](https://cfhammill.github.io/lenses/reference/select_l.html) \nis bidirectional. This means you can \n[`set`](https://cfhammill.github.io/lenses/reference/set.html) the results \nof your selection.\n\nlet's select columns between `Sepal.Width` and `Petal.Width` and increment them by 10:\n\n```{r}\niris %\u003e%\n  over(select_l(Sepal.Width:Petal.Width)\n     , ~ . + 10\n       ) %\u003e%\n  head(3)\n```\n\nNot only does [`select_l`](https://cfhammill.github.io/lenses/reference/select_l.html) \ncreate the appropriate lens for you with `dplyr::select` style column references, \nbut [`over`](https://cfhammill.github.io/lenses/reference/over.html) allows us \nto declare anonymous functions like in `purrr`.\n\nAt this point I can imagine you saying, all this is very clear, but what good is it, \nI have `mutate`. Well that is a good point. It is hard to beat the convenience\nof `mutate`. However, `select_l` has an advantage, it can be used on any named\nobject:\n\n```{r}\niris %\u003e%\n  as.list %\u003e%\n  view(select_l(matches(\"Sepal\")) %.%\n       index(1) %.%\n       index(1)\n       ) \n```\n\nYou can use it with vectors, lists, data.frames, etc.\n\nIf `select_l` isn't enticing enough, have you ever wanted to `set` \nor modify the results of a `filter`?  This is not super easy to do in the `dplyr` \nuniverse. But our `lens`ified `filter`, \n[`filter_l`](https://cfhammill.github.io/lenses/reference/filter_l.html) does this \nwith ease.\n\nLet's set all \"Sepal\" columns where the row number is less than three to \nzero. And for fun let's also change the column names to all upper case:\n\n```{r}\nlibrary(dplyr)\n\niris %\u003e%\n  mutate(row_num = seq_len(n())) %\u003e%\n  set(filter_l(row_num \u003c 3) %.%\n      select_l(matches(\"Sepal\"))\n    , 0) %\u003e%\n  over(names_l, toupper) %\u003e%\n  head(3)\n```\n\nYou can even use mutate `over` your `filter_l`\n\n```{r}\niris %\u003e%\n  mutate(row_num = seq_len(n())) %\u003e%\n  over(filter_l(row_num \u003c 3)\n     , ~ mutate(., Sepal.Length = 0)) %\u003e%\n  head(3)\n```\n\nAs you can see, lenses can be smoothly integrated into your `tidyverse`\nworkflows, as well as your base R workflows. Giving you the powers\nof compositionality and bidirectionality to improve your code.\n\n## Mapping lenses\n\nFrequently we end up in situations where we want to modify each element\nof a nested object. This is especially cumbersome without lenses. Let's\nimagine our data lives inside a larger structure. And additionally that\nit isn't a nice data frame, but a list.\n\n```{r}\npacked_iris \u003c- list(as.list(iris))\n\npacked_iris %\u003e% str(2)\n```\n\nsay I want to add 10 to the first element of each column between `Sepal.Length` and\n`Petal.Width`. Base R I might do something like:\n\n```{r}\nels_of_interest \u003c-\n  grep(\"Sepal|Petal\", names(packed_iris[[1]]), value = TRUE)\n\npacked_iris[[1]][1:4] \u003c-\n  lapply(packed_iris[[1]][1:4]\n       , function(x){ x[1] \u003c- x[1] + 10; x })\n\nstr(packed_iris, 2)\n```\n\npretty ugly right?\n\nTo do this with lenses we can use the `map_l` function to promote a `lens` to\napply to each element of a list.\n\n```{r}\nels_l \u003c-\n  index(1) %.%\n  select_l(Sepal.Length:Petal.Width) %.%\n  map_l(index(1))\n\nover_map(packed_iris, els_l, ~ . + 10) %\u003e%\n  str(2)\n```\n\nHere we use the `over_map` function to apply a function to each element,\nyou could equivalently use `over` with `lapply` as well. As you can see\nsetting and applying functions to multiple elements of nested data is\ndramatically improved by using lenses.\n\n## Polishing your own\n\nYou can make a lens from scratch (!) by passing `view` and `set` functions to \nthe [`lens`](https://cfhammill.github.io/lenses/reference/lens.html) constructor:\n\n```{r}\nfirst_l \u003c- lens(view = function(d) d[[1]],\n                set  = function(d, x) { d[[1]] \u003c- x; d })\n```\n\nAs you can see, the `view` function must accept an element of data, while the `set`\nfunction must accept such an element as well as the new value of the subpart,\nand return the new data in its entirety - thus achieving composability -\n without modifying the original.\n\nIn order to avoid unpleasant surprises or inconsistencies for users,\nan author of a `lens` (via [`lens`](https://cfhammill.github.io/lenses/reference/lens.html)) \nshould ensure it obeys the following rules (the \"Lenz laws\", here paraphrased \nfrom [a Haskell lens tutorial](www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial)):\n\n1. View-Set: If you `view` some data with a lens, and then\n`set` the data with that value, you get the input data back.\n2. Set-View: If you `set` a value with a lens,\nthen `view` that value with the same lens, you get back what you put in.\n3. Set-Set: If you `set` a value into some data with a lens, and\nthen `set` another value with the same lens, it's the same as only\ndoing the second `set`.\n\n\"Lenses\" which do not satisfy these properties should be documented accordingly.\nBy convention, the few such specimens in this library are suffixed by \"_il\" (\"illegal lens\").\nSee the package [reference](https://cfhammill.github.io/lenses/reference/) for more.\n\n## How do they work?\n\nAs you can see from the `lens` constructor, knowing how to implement `view` and `set`\nfor a lens turns out to be sufficient to implement the other verbs such as `over` and\n- most importantly - lens composition (`%.%`). \n\nIn our implementation, lenses are trivial. They simply store the provided\nfunctions. A `lens` under the hood is a two element list with an \nelement `view` and an element `set`.\n\n## History of lens making\n\nThere is nothing particularly new about the lenses appearing here.\nFor a fairly comprehensive (and highly technical) history of lenses, see\n[links here](https://github.com/ekmett/lens/wiki/History-of-Lenses) and\n[this blog post](https://julesh.com/2018/08/16/lenses-for-philosophers/) .\n\n---\n\nThanks to Leigh Spencer Noakes, Zsu Lindenmaier, and Lily Qiu for reading \ndrafts of this document and providing very helpful feedback.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcfhammill%2Flenses","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcfhammill%2Flenses","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcfhammill%2Flenses/lists"}