Skip to content

Commit

Permalink
Added blog post about ConcertAI experience
Browse files Browse the repository at this point in the history
  • Loading branch information
TinasheMTapera committed Aug 28, 2024
1 parent f716ddd commit b644f44
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"hash": "f3e4e8b1bc2ff684a6dd1e5409a8a212",
"hash": "847f8e211dfa5cf841b40c1951a67b9d",
"result": {
"markdown": "---\ntitle: \"Managing Expected Loop Failures with Purrr\"\nauthor: \"Tinashe M. Tapera\"\ndate: \"2022-08-21\"\nimage: infinite_loop.mp4\n---\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(dplyr)\nlibrary(purrr)\n\nset.seed(12345)\n```\n:::\n\n\nLooping is a fundamental programming paradigm. You have a set of inputs,\nand you wanna run a function on each of them:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ninputs <- sample(1:100, 10)\n\nadd_ten <- function(x) {\n return(x + 10)\n}\n\n# base R looping\noutputs <- c()\nfor(x in inputs){\n outputs <- c(outputs, (add_ten(x)))\n}\n# \\loop\n\nprint(glue::glue(\"{inputs} -> {outputs}\"))\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n14 -> 24\n51 -> 61\n80 -> 90\n90 -> 100\n92 -> 102\n24 -> 34\n58 -> 68\n93 -> 103\n75 -> 85\n88 -> 98\n```\n:::\n:::\n\n\nWith the `purrr` library, we get the same functionality as looping^[In truth, `purrr` doesn't implement a `for` or `while` loop; it's actually a tidy implementation of the `*apply` family of functions. Awesome!]\nbut with an arguably friendlier interface and more compliant mechanics with the\nidiosyncracies of the `tidyverse`:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# much nicer!\noutputs <- map(inputs, add_ten)\nprint(glue::glue(\"{inputs} -> {outputs}\"))\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n14 -> 24\n51 -> 61\n80 -> 90\n90 -> 100\n92 -> 102\n24 -> 34\n58 -> 68\n93 -> 103\n75 -> 85\n88 -> 98\n```\n:::\n:::\n\n\nThis is all fine and dandy, but let's say you get a failure from the data,\nlike, `add_ten` throws an error if the output is greater than 100:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten <- function(x) {\n output <- x + 10\n if(output > 100){\n stop(\"The output is too great!\")\n }\n return(output)\n}\n```\n:::\n\n\nIn a for loop, this fails as expected:\n\n\n::: {.cell}\n\n```{.r .cell-code}\noutputs <- c()\nfor(x in inputs){\n outputs <- c(outputs, (add_ten(x)))\n}\n```\n\n::: {.cell-output .cell-output-error}\n```\nError in add_ten(x): The output is too great!\n```\n:::\n:::\n\n\nIf I had to debug it this code, I would probably set up an iterator:\n\n\n::: {.cell}\n\n```{.r .cell-code}\noutputs <- c()\nfor(x in 1:length(inputs)){\n print(x)\n outputs <- c(outputs, (add_ten(inputs[x])))\n}\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[1] 1\n[1] 2\n[1] 3\n[1] 4\n[1] 5\n```\n:::\n\n::: {.cell-output .cell-output-error}\n```\nError in add_ten(inputs[x]): The output is too great!\n```\n:::\n:::\n\n\nIt failed at 5, so I'll check `inputs[5]` and debug:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ninputs[5]\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[1] 92\n```\n:::\n\n```{.r .cell-code}\n# the output would be greater than 100! Duh!!\n```\n:::\n\n\nBut with `purrr::map()`, there isn't a straightforward way to debug like this^[I'm lying of course; there is `imap`; but that's not why we're here today].\nAnd if you have a large dataset, with a long-running function, you probably\ndon't want to wait until the `map` call fails and you have to go digging around\ninto exactly _which_ object in the vector had the problem.\n\n**Enter: `safely()` and `possibly()`.**\n\nThese are two functions that modify the behaviour of a `purrr`\ncall. You can wrap your function in one of these, and `purrr` will give you\na couple of ways of managing what happens if and when your loop fails or throws\nsome kind of warning or unexpected output. Here's an example with the `add_ten`\nfunction, using `quietly()` to force `map` to keep going even if there's a failure:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten <- function(x) {\n output <- x + 10\n if(output > 100){\n stop(\"The output is too great!\")\n }\n return(output)\n}\n\nadd_ten_safely <- safely(add_ten)\n```\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\nout <- add_ten_safely(10)\nout\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n$result\n[1] 20\n\n$error\nNULL\n```\n:::\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\nout <- add_ten_safely(95)\nout\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n$result\nNULL\n\n$error\n<simpleError in .f(...): The output is too great!>\n```\n:::\n:::\n\n\nHere, we see that `safely()` returns a list of outputs, with `result` and `error`.\nImplementing this in our `dplyr` chain would thus look like:\n\n\n::: {.cell}\n\n```{.r .cell-code}\noutputs <- map(inputs, add_ten_safely)\noutputs\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1]]\n[[1]]$result\n[1] 24\n\n[[1]]$error\nNULL\n\n\n[[2]]\n[[2]]$result\n[1] 61\n\n[[2]]$error\nNULL\n\n\n[[3]]\n[[3]]$result\n[1] 90\n\n[[3]]$error\nNULL\n\n\n[[4]]\n[[4]]$result\n[1] 100\n\n[[4]]$error\nNULL\n\n\n[[5]]\n[[5]]$result\nNULL\n\n[[5]]$error\n<simpleError in .f(...): The output is too great!>\n\n\n[[6]]\n[[6]]$result\n[1] 34\n\n[[6]]$error\nNULL\n\n\n[[7]]\n[[7]]$result\n[1] 68\n\n[[7]]$error\nNULL\n\n\n[[8]]\n[[8]]$result\nNULL\n\n[[8]]$error\n<simpleError in .f(...): The output is too great!>\n\n\n[[9]]\n[[9]]$result\n[1] 85\n\n[[9]]$error\nNULL\n\n\n[[10]]\n[[10]]$result\n[1] 98\n\n[[10]]$error\nNULL\n```\n:::\n:::\n\n\nWhat if we want to have a default value returned if there is an error?\nWell, in base R we'd do something like this:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten_w_error_base <- function(x) {\n output <- x + 10\n if(output > 100){\n # send a message to the console as a side effect\n message(\"The output is too great!\") \n # return a value\n return(NA)\n }\n return(output)\n}\n\noutputs <- map(inputs, add_ten_w_error_base)\n```\n\n::: {.cell-output .cell-output-stderr}\n```\nThe output is too great!\nThe output is too great!\n```\n:::\n\n```{.r .cell-code}\noutputs\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1]]\n[1] 24\n\n[[2]]\n[1] 61\n\n[[3]]\n[1] 90\n\n[[4]]\n[1] 100\n\n[[5]]\n[1] NA\n\n[[6]]\n[1] 34\n\n[[7]]\n[1] 68\n\n[[8]]\n[1] NA\n\n[[9]]\n[1] 85\n\n[[10]]\n[1] 98\n```\n:::\n:::\n\n\nBut in `purrr`, `safely()` comes with the option to just specify this in the\nfunction with the `otherwise` argument! Check it out:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten_safely <- safely(add_ten, otherwise = NA)\n\noutputs <- map(inputs, add_ten_safely)\noutputs\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1]]\n[[1]]$result\n[1] 24\n\n[[1]]$error\nNULL\n\n\n[[2]]\n[[2]]$result\n[1] 61\n\n[[2]]$error\nNULL\n\n\n[[3]]\n[[3]]$result\n[1] 90\n\n[[3]]$error\nNULL\n\n\n[[4]]\n[[4]]$result\n[1] 100\n\n[[4]]$error\nNULL\n\n\n[[5]]\n[[5]]$result\n[1] NA\n\n[[5]]$error\n<simpleError in .f(...): The output is too great!>\n\n\n[[6]]\n[[6]]$result\n[1] 34\n\n[[6]]$error\nNULL\n\n\n[[7]]\n[[7]]$result\n[1] 68\n\n[[7]]$error\nNULL\n\n\n[[8]]\n[[8]]$result\n[1] NA\n\n[[8]]$error\n<simpleError in .f(...): The output is too great!>\n\n\n[[9]]\n[[9]]$result\n[1] 85\n\n[[9]]$error\nNULL\n\n\n[[10]]\n[[10]]$result\n[1] 98\n\n[[10]]$error\nNULL\n```\n:::\n:::\n\n\nThis is very useful! What's more, the `possibly()` function defaults to only\nreturning the successful result _or_ the error condition, so you don't even\nhave to deal with a janky list output:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten_possibly <- possibly(add_ten, otherwise = NA)\n\noutputs <- map(inputs, add_ten_possibly)\noutputs\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1]]\n[1] 24\n\n[[2]]\n[1] 61\n\n[[3]]\n[1] 90\n\n[[4]]\n[1] 100\n\n[[5]]\n[1] NA\n\n[[6]]\n[1] 34\n\n[[7]]\n[1] 68\n\n[[8]]\n[1] NA\n\n[[9]]\n[1] 85\n\n[[10]]\n[1] 98\n```\n:::\n:::\n\n\nWhich is easily parseable:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nunlist(outputs)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n [1] 24 61 90 100 NA 34 68 NA 85 98\n```\n:::\n:::\n\n\n## Why Is This Useful\n\nI'd say this is a useful family of functions in a limited handful of scenarios, but comes\nin clutch when you meet them. When I first tried these functions out,\nI was processing a number of input files (n < 1000) with an external Matlab\nfunction that read in the file, calculated a parameter, and sent it back to R.\nIn my experience, this approach was great because I 1) a long-ish list of\ninputs to a function, 2) had a function that took around 5-10 minutes\nto run, per input, and 3) had an expected failure case that I didn't much\ncare about (parameter inputs were sometimes invalid) and predictable/not unexpected,\nso I didn't quite want to handle them with a within-function `tryCatch` strategy. \n\nIn fact, most programmers (probably Python folks) are probably asking right now, \n\"why would't you just use a `tryCatch` and not deal with another dependency?\"\n\nWell, the answer is that I think with this method, we keep the functions much\nmore compact and straightforward, while also acknowledging that I will get\nerrors returned when I expect them. This would be an unsafe approach when I\ndo not know what inputs are expected, and what exactly can go wrong. But on this\nparticular afternoon at work, I knew pretty much _every_ input dataset, and\nknew/didn't care about the reasons for a failure of the processing. I felt that\nthis scenario lended itself well to the _prima facie_, handwavy approach of\nusing `otherwise` in what's essentially an `apply` call with syntactic sugar.\n\nSo, the lesson here is, use `purrr` functions instead of your loops. Or don't,\nI guess. I'm not the expert here. I was honestly just tired and needed a better\nsolution than \"check each of these files for the different errors they could throw\", and for that, `purrr` worked out perfectly.\n\nAnyway, here's a perfect loop to summarise this blog post. Any loop can be\nperfect, but when they are, they're kinda freaky. Best to expect some failures.\n\n![](https://thumbs.gfycat.com/BiodegradableDefensiveAiredale-mobile.mp4)\n",
"markdown": "---\ntitle: \"Managing Expected Loop Failures with Purrr\"\nauthor: \"Tinashe M. Tapera\"\ndate: \"2022-08-21\"\nimage: https://media0.giphy.com/media/jyEpo3XfNNghuMubJO/200w.gif?cid=6c09b952zy0kglvx4sq8g1znubrr2lidaa879xlsnqpdfexu&ep=v1_videos_search&rid=200w.gif&ct=v\n---\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(dplyr)\nlibrary(purrr)\n\nset.seed(12345)\n```\n:::\n\n\nLooping is a fundamental programming paradigm. You have a set of inputs,\nand you wanna run a function on each of them:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ninputs <- sample(1:100, 10)\n\nadd_ten <- function(x) {\n return(x + 10)\n}\n\n# base R looping\noutputs <- c()\nfor(x in inputs){\n outputs <- c(outputs, (add_ten(x)))\n}\n# \\loop\n\nprint(glue::glue(\"{inputs} -> {outputs}\"))\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n14 -> 24\n51 -> 61\n80 -> 90\n90 -> 100\n92 -> 102\n24 -> 34\n58 -> 68\n93 -> 103\n75 -> 85\n88 -> 98\n```\n:::\n:::\n\n\nWith the `purrr` library, we get the same functionality as looping^[In truth, `purrr` doesn't implement a `for` or `while` loop; it's actually a tidy implementation of the `*apply` family of functions. Awesome!]\nbut with an arguably friendlier interface and more compliant mechanics with the\nidiosyncracies of the `tidyverse`:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# much nicer!\noutputs <- map(inputs, add_ten)\nprint(glue::glue(\"{inputs} -> {outputs}\"))\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n14 -> 24\n51 -> 61\n80 -> 90\n90 -> 100\n92 -> 102\n24 -> 34\n58 -> 68\n93 -> 103\n75 -> 85\n88 -> 98\n```\n:::\n:::\n\n\nThis is all fine and dandy, but let's say you get a failure from the data,\nlike, `add_ten` throws an error if the output is greater than 100:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten <- function(x) {\n output <- x + 10\n if(output > 100){\n stop(\"The output is too great!\")\n }\n return(output)\n}\n```\n:::\n\n\nIn a for loop, this fails as expected:\n\n\n::: {.cell}\n\n```{.r .cell-code}\noutputs <- c()\nfor(x in inputs){\n outputs <- c(outputs, (add_ten(x)))\n}\n```\n\n::: {.cell-output .cell-output-error}\n```\nError in add_ten(x): The output is too great!\n```\n:::\n:::\n\n\nIf I had to debug it this code, I would probably set up an iterator:\n\n\n::: {.cell}\n\n```{.r .cell-code}\noutputs <- c()\nfor(x in 1:length(inputs)){\n print(x)\n outputs <- c(outputs, (add_ten(inputs[x])))\n}\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[1] 1\n[1] 2\n[1] 3\n[1] 4\n[1] 5\n```\n:::\n\n::: {.cell-output .cell-output-error}\n```\nError in add_ten(inputs[x]): The output is too great!\n```\n:::\n:::\n\n\nIt failed at 5, so I'll check `inputs[5]` and debug:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ninputs[5]\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[1] 92\n```\n:::\n\n```{.r .cell-code}\n# the output would be greater than 100! Duh!!\n```\n:::\n\n\nBut with `purrr::map()`, there isn't a straightforward way to debug like this^[I'm lying of course; there is `imap`; but that's not why we're here today].\nAnd if you have a large dataset, with a long-running function, you probably\ndon't want to wait until the `map` call fails and you have to go digging around\ninto exactly _which_ object in the vector had the problem.\n\n**Enter: `safely()` and `possibly()`.**\n\nThese are two functions that modify the behaviour of a `purrr`\ncall. You can wrap your function in one of these, and `purrr` will give you\na couple of ways of managing what happens if and when your loop fails or throws\nsome kind of warning or unexpected output. Here's an example with the `add_ten`\nfunction, using `quietly()` to force `map` to keep going even if there's a failure:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten <- function(x) {\n output <- x + 10\n if(output > 100){\n stop(\"The output is too great!\")\n }\n return(output)\n}\n\nadd_ten_safely <- safely(add_ten)\n```\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\nout <- add_ten_safely(10)\nout\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n$result\n[1] 20\n\n$error\nNULL\n```\n:::\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\nout <- add_ten_safely(95)\nout\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n$result\nNULL\n\n$error\n<simpleError in .f(...): The output is too great!>\n```\n:::\n:::\n\n\nHere, we see that `safely()` returns a list of outputs, with `result` and `error`.\nImplementing this in our `dplyr` chain would thus look like:\n\n\n::: {.cell}\n\n```{.r .cell-code}\noutputs <- map(inputs, add_ten_safely)\noutputs\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1]]\n[[1]]$result\n[1] 24\n\n[[1]]$error\nNULL\n\n\n[[2]]\n[[2]]$result\n[1] 61\n\n[[2]]$error\nNULL\n\n\n[[3]]\n[[3]]$result\n[1] 90\n\n[[3]]$error\nNULL\n\n\n[[4]]\n[[4]]$result\n[1] 100\n\n[[4]]$error\nNULL\n\n\n[[5]]\n[[5]]$result\nNULL\n\n[[5]]$error\n<simpleError in .f(...): The output is too great!>\n\n\n[[6]]\n[[6]]$result\n[1] 34\n\n[[6]]$error\nNULL\n\n\n[[7]]\n[[7]]$result\n[1] 68\n\n[[7]]$error\nNULL\n\n\n[[8]]\n[[8]]$result\nNULL\n\n[[8]]$error\n<simpleError in .f(...): The output is too great!>\n\n\n[[9]]\n[[9]]$result\n[1] 85\n\n[[9]]$error\nNULL\n\n\n[[10]]\n[[10]]$result\n[1] 98\n\n[[10]]$error\nNULL\n```\n:::\n:::\n\n\nWhat if we want to have a default value returned if there is an error?\nWell, in base R we'd do something like this:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten_w_error_base <- function(x) {\n output <- x + 10\n if(output > 100){\n # send a message to the console as a side effect\n message(\"The output is too great!\") \n # return a value\n return(NA)\n }\n return(output)\n}\n\noutputs <- map(inputs, add_ten_w_error_base)\n```\n\n::: {.cell-output .cell-output-stderr}\n```\nThe output is too great!\nThe output is too great!\n```\n:::\n\n```{.r .cell-code}\noutputs\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1]]\n[1] 24\n\n[[2]]\n[1] 61\n\n[[3]]\n[1] 90\n\n[[4]]\n[1] 100\n\n[[5]]\n[1] NA\n\n[[6]]\n[1] 34\n\n[[7]]\n[1] 68\n\n[[8]]\n[1] NA\n\n[[9]]\n[1] 85\n\n[[10]]\n[1] 98\n```\n:::\n:::\n\n\nBut in `purrr`, `safely()` comes with the option to just specify this in the\nfunction with the `otherwise` argument! Check it out:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten_safely <- safely(add_ten, otherwise = NA)\n\noutputs <- map(inputs, add_ten_safely)\noutputs\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1]]\n[[1]]$result\n[1] 24\n\n[[1]]$error\nNULL\n\n\n[[2]]\n[[2]]$result\n[1] 61\n\n[[2]]$error\nNULL\n\n\n[[3]]\n[[3]]$result\n[1] 90\n\n[[3]]$error\nNULL\n\n\n[[4]]\n[[4]]$result\n[1] 100\n\n[[4]]$error\nNULL\n\n\n[[5]]\n[[5]]$result\n[1] NA\n\n[[5]]$error\n<simpleError in .f(...): The output is too great!>\n\n\n[[6]]\n[[6]]$result\n[1] 34\n\n[[6]]$error\nNULL\n\n\n[[7]]\n[[7]]$result\n[1] 68\n\n[[7]]$error\nNULL\n\n\n[[8]]\n[[8]]$result\n[1] NA\n\n[[8]]$error\n<simpleError in .f(...): The output is too great!>\n\n\n[[9]]\n[[9]]$result\n[1] 85\n\n[[9]]$error\nNULL\n\n\n[[10]]\n[[10]]$result\n[1] 98\n\n[[10]]$error\nNULL\n```\n:::\n:::\n\n\nThis is very useful! What's more, the `possibly()` function defaults to only\nreturning the successful result _or_ the error condition, so you don't even\nhave to deal with a janky list output:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_ten_possibly <- possibly(add_ten, otherwise = NA)\n\noutputs <- map(inputs, add_ten_possibly)\noutputs\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n[[1]]\n[1] 24\n\n[[2]]\n[1] 61\n\n[[3]]\n[1] 90\n\n[[4]]\n[1] 100\n\n[[5]]\n[1] NA\n\n[[6]]\n[1] 34\n\n[[7]]\n[1] 68\n\n[[8]]\n[1] NA\n\n[[9]]\n[1] 85\n\n[[10]]\n[1] 98\n```\n:::\n:::\n\n\nWhich is easily parseable:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nunlist(outputs)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n [1] 24 61 90 100 NA 34 68 NA 85 98\n```\n:::\n:::\n\n\n## Why Is This Useful\n\nI'd say this is a useful family of functions in a limited handful of scenarios, but comes\nin clutch when you meet them. When I first tried these functions out,\nI was processing a number of input files (n < 1000) with an external Matlab\nfunction that read in the file, calculated a parameter, and sent it back to R.\nIn my experience, this approach was great because I 1) a long-ish list of\ninputs to a function, 2) had a function that took around 5-10 minutes\nto run, per input, and 3) had an expected failure case that I didn't much\ncare about (parameter inputs were sometimes invalid) and predictable/not unexpected,\nso I didn't quite want to handle them with a within-function `tryCatch` strategy. \n\nIn fact, most programmers (probably Python folks) are probably asking right now, \n\"why would't you just use a `tryCatch` and not deal with another dependency?\"\n\nWell, the answer is that I think with this method, we keep the functions much\nmore compact and straightforward, while also acknowledging that I will get\nerrors returned when I expect them. This would be an unsafe approach when I\ndo not know what inputs are expected, and what exactly can go wrong. But on this\nparticular afternoon at work, I knew pretty much _every_ input dataset, and\nknew/didn't care about the reasons for a failure of the processing. I felt that\nthis scenario lended itself well to the _prima facie_, handwavy approach of\nusing `otherwise` in what's essentially an `apply` call with syntactic sugar.\n\nSo, the lesson here is, use `purrr` functions instead of your loops. Or don't,\nI guess. I'm not the expert here. I was honestly just tired and needed a better\nsolution than \"check each of these files for the different errors they could throw\", and for that, `purrr` worked out perfectly.\n\nAnyway, here's a perfect loop to summarise this blog post. Any loop can be\nperfect, but when they are, they're kinda freaky. Best to expect some failures.\n\n<blockquote class=\"reddit-embed-bq\" style=\"height:500px\" data-embed-height=\"740\"><a href=\"https://www.reddit.com/r/perfectloops/comments/184mtrv/missed_a_spot_l/\">Missed a spot... [L]</a><br> by<a href=\"https://www.reddit.com/user/igneus/\">u/igneus</a> in<a href=\"https://www.reddit.com/r/perfectloops/\">perfectloops</a></blockquote><script async=\"\" src=\"https://embed.reddit.com/widgets.js\" charset=\"UTF-8\"></script>\n",
"supporting": [],
"filters": [
"rmarkdown/pagebreak.lua"
Expand Down
Loading

0 comments on commit b644f44

Please sign in to comment.