From 1efd49ee894f8b6c9841c35557b4f1d99bd74ca3 Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Tue, 25 Jun 2024 13:21:18 +0200 Subject: [PATCH 01/14] Start graceful degradation post --- posts/graceful-degradation/index.qmd | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 posts/graceful-degradation/index.qmd diff --git a/posts/graceful-degradation/index.qmd b/posts/graceful-degradation/index.qmd new file mode 100644 index 00000000..1fc032c5 --- /dev/null +++ b/posts/graceful-degradation/index.qmd @@ -0,0 +1,31 @@ +--- +title: "Improving Ecosystem Interoperability Iteratively via Progressive Enhancement" +author: + - name: "Hugo Gruson" + orcid: "0000-0002-4094-1476" +date: "2024-01-31" +categories: [R, reproducibility, renv] +format: + html: + toc: true +--- + +We are continuing on post series on S3 and interoperability. +We have previously discussed what makes a good S3 class and how to choose a good parent for it, as well as when to write or not write a custom method. +We have highlighted in particular how classes inheriting from data.frames can simplify user experience, and reduce developer workload. + +We have detailed how to improve compatibility with the tidyverse by explaining: + +- how functions taking data.frames or data.frames subclass can easily and should also allow compatibility with tibble +- how to ensure class attribute is preserved whenever possible while using dplyr functions. + +Here, we are going to explore how to actually start adding support in the ecosystem for the newly S3 classes while minimizing user-facing breaking changes. +We have previously delved into this topic with our previous post "" and this is a wider and higher-level view of the same topic. + +The strategy presented here is the declination of a common concept in web development and the web ecosystem: progressive enhancement. This philosophy aims to support browsers with limited range of features, while allowing a slightly richer experience for browsers with extra features. +It makes sense to think about this philosophy with the prism of introducing new classes to a new software ecosystem as it has the similar constraints of multiple stakeholders with different interests and timelines. +The application of progressive enhancement in this context means that users or packages that did not (yet) adopt the new classes are not penalized compared to users or packages that did. + +## Adding class support to function inputs via progressive enhancement + +## Adding class support to function outputs via progressive enhancement From 0970812aef693caac5c88dd4948f5e1a7135c2a3 Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:34:10 +0200 Subject: [PATCH 02/14] Finalize progressive enhancement post --- posts/graceful-degradation/index.qmd | 31 ----- .../convert_to_generic.svg | 126 ++++++++++++++++++ posts/progressive-enhancement/index.qmd | 58 ++++++++ 3 files changed, 184 insertions(+), 31 deletions(-) delete mode 100644 posts/graceful-degradation/index.qmd create mode 100644 posts/progressive-enhancement/convert_to_generic.svg create mode 100644 posts/progressive-enhancement/index.qmd diff --git a/posts/graceful-degradation/index.qmd b/posts/graceful-degradation/index.qmd deleted file mode 100644 index 1fc032c5..00000000 --- a/posts/graceful-degradation/index.qmd +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Improving Ecosystem Interoperability Iteratively via Progressive Enhancement" -author: - - name: "Hugo Gruson" - orcid: "0000-0002-4094-1476" -date: "2024-01-31" -categories: [R, reproducibility, renv] -format: - html: - toc: true ---- - -We are continuing on post series on S3 and interoperability. -We have previously discussed what makes a good S3 class and how to choose a good parent for it, as well as when to write or not write a custom method. -We have highlighted in particular how classes inheriting from data.frames can simplify user experience, and reduce developer workload. - -We have detailed how to improve compatibility with the tidyverse by explaining: - -- how functions taking data.frames or data.frames subclass can easily and should also allow compatibility with tibble -- how to ensure class attribute is preserved whenever possible while using dplyr functions. - -Here, we are going to explore how to actually start adding support in the ecosystem for the newly S3 classes while minimizing user-facing breaking changes. -We have previously delved into this topic with our previous post "" and this is a wider and higher-level view of the same topic. - -The strategy presented here is the declination of a common concept in web development and the web ecosystem: progressive enhancement. This philosophy aims to support browsers with limited range of features, while allowing a slightly richer experience for browsers with extra features. -It makes sense to think about this philosophy with the prism of introducing new classes to a new software ecosystem as it has the similar constraints of multiple stakeholders with different interests and timelines. -The application of progressive enhancement in this context means that users or packages that did not (yet) adopt the new classes are not penalized compared to users or packages that did. - -## Adding class support to function inputs via progressive enhancement - -## Adding class support to function outputs via progressive enhancement diff --git a/posts/progressive-enhancement/convert_to_generic.svg b/posts/progressive-enhancement/convert_to_generic.svg new file mode 100644 index 00000000..7bad9c3b --- /dev/null +++ b/posts/progressive-enhancement/convert_to_generic.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd new file mode 100644 index 00000000..6834e2f9 --- /dev/null +++ b/posts/progressive-enhancement/index.qmd @@ -0,0 +1,58 @@ +--- +title: "Improving Ecosystem Interoperability Iteratively via Progressive Enhancement" +author: + - name: "Hugo Gruson" + orcid: "0000-0002-4094-1476" +date: "2024-07-04" +categories: [R, interoperability, S3, progressive enhancement, ecosystem, lifecycle, object orientation] +format: + html: + toc: true +--- + +We are continuing on post series on S3 and interoperability. +We have previously discussed [what makes a good S3 class and how to choose a good parent for it, as well as when to write or not write a custom method](../parent-class). +We have highlighted in particular how classes inheriting from data.frames can simplify user experience, and reduce developer workload. + +We have detailed how to improve compatibility with the tidyverse by explaining: + +- [how functions taking data.frames or data.frames subclass can easily and should also allow compatibility with tibble](https://hugogruson.fr/posts/compa-tibble/) +- [how to ensure class attribute is preserved whenever possible while using dplyr functions](../extend-dataframes). + +Here, we are going to explore how to actually start adding support in the ecosystem for the newly S3 classes while minimizing user-facing breaking changes. +We have previously delved into this topic with our previous post ["Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations"](../s3-generic) and this is a wider and higher-level view of the same topic. + +The strategy presented here is the declination of a common concept in web development and the web ecosystem: [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). This philosophy aims to support browsers with limited range of features, while allowing a slightly richer experience for browsers with extra features. +It makes sense to think about this philosophy with the prism of introducing new classes to a new software ecosystem as it has the similar constraints of multiple stakeholders with different interests and timelines. +The application of progressive enhancement in this context means that users or packages that did not (yet) adopt the new classes are not penalized compared to users or packages that did. + +## Adding class support to function inputs via progressive enhancement + +The goal here is to allow functions to accept the new classes as inputs, while keeping the old behaviour unchanged for unclassed objects (or with a different class than the new one). + +This can conveniently be done in an almost transparent way by converting the old function to the S3 generic, and using the default method to handle the old behaviour. The practical steps, and minor caveats, have been previously described in the post ["Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations"](../s3-generic). + +![A before / after type image showing the conversion of a function to a generic with a default method keeping the exisiting behaviour.](convert_to_generic.svg) + +If the function was already a generic, even steps are required: a new method for the new class should be added, leaving everything else unchanged. + +## Adding class support to function outputs via progressive enhancement + +Adding class support to function outputs is often more challenging. +A common option is to add a new argument to the function, which would be a boolean indicating whether the output should be of the new class or not. +But this doesn't fit in the view of progressive enhancement, as it would require users to change their code to benefit from the new classes, or to suffer from breaking changes. + +While the new argument approach is sometimes indeed the only possible method, there are some situations where we can have an approach truly following the progressive enhancement philosophy. + +In particular, this is the case when the old output was already inheriting from the parent of the new class (hence the importance of carefully choosing the parent class). In this situation, the new attributes from the new class should not interfere with existing code for downstream analysis. + +This is however only true if code in downstream analysis follows good practices [^1]. If existing code was testing equality of the class to a certain value, it will break when the new class value is appended. This is described in a [post on the R developer blog, when base R was adding a new `array` class value to `matrix` objects](https://developer.r-project.org/Blog/public/2019/11/09/when-you-think-class.-think-again/index.html). Class inheritance should never be tested via `class(x) == "some_class"`. Instead, `inherits(x, "some_class")` or `is(x, "some_class")` should be used to future-proof the code and allow appending an additional in the future. + +[^1]: This is now [enforced in R packages by R CMD check](https://github.com/r-devel/r-svn/commit/77ebdff5adc200dfe9bc850bc4447088830d2ee0), and via the [`class_equals_linter()`](https://lintr.r-lib.org/reference/class_equals_linter.html) in the [lintr package](https://lintr.r-lib.org/). + +## Conclusion + +Object oriented programming and S3 classes offer a convenient way to iteratively add interoperability in the ecosystem in a way that is minimally disruptive to users and developers. +Newly classed input support can be added via custom methods (after converting the existing function to a generic if necessary). +Newly classed output support can be added via progressive enhancement, by ensuring that the new class is a subclass of the old one, that downstream code uses good practices. + From e06b04d2ccefcb6c4df8318eaf3213d5648b4e0d Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:36:41 +0200 Subject: [PATCH 03/14] Fix lints --- posts/progressive-enhancement/index.qmd | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index 6834e2f9..6209d933 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -10,7 +10,7 @@ format: toc: true --- -We are continuing on post series on S3 and interoperability. +We are continuing on post series on S3 and interoperability. We have previously discussed [what makes a good S3 class and how to choose a good parent for it, as well as when to write or not write a custom method](../parent-class). We have highlighted in particular how classes inheriting from data.frames can simplify user experience, and reduce developer workload. @@ -55,4 +55,3 @@ This is however only true if code in downstream analysis follows good practices Object oriented programming and S3 classes offer a convenient way to iteratively add interoperability in the ecosystem in a way that is minimally disruptive to users and developers. Newly classed input support can be added via custom methods (after converting the existing function to a generic if necessary). Newly classed output support can be added via progressive enhancement, by ensuring that the new class is a subclass of the old one, that downstream code uses good practices. - From 8a4608954207587f3c071fc24935a3ca938fd22b Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:01:50 +0200 Subject: [PATCH 04/14] Add DOI category --- posts/progressive-enhancement/index.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index 6209d933..d4954e94 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -4,7 +4,7 @@ author: - name: "Hugo Gruson" orcid: "0000-0002-4094-1476" date: "2024-07-04" -categories: [R, interoperability, S3, progressive enhancement, ecosystem, lifecycle, object orientation] +categories: [R, interoperability, S3, progressive enhancement, ecosystem, lifecycle, object orientation, DOI] format: html: toc: true From 6b57afae54d5ec33633f34bfe6716cbb7b4df42f Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 07:35:49 +0000 Subject: [PATCH 05/14] Use better phrasing from James' review Co-authored-by: James Azam --- posts/progressive-enhancement/index.qmd | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index d4954e94..2ff7fee7 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -10,17 +10,17 @@ format: toc: true --- -We are continuing on post series on S3 and interoperability. +We are continuing our post series on S3 and interoperability. We have previously discussed [what makes a good S3 class and how to choose a good parent for it, as well as when to write or not write a custom method](../parent-class). -We have highlighted in particular how classes inheriting from data.frames can simplify user experience, and reduce developer workload. +We have highlighted in particular how classes inheriting from data.frames can simplify user experience because of familiarity, and reduce developer workload due to the pre-existing S3 methods. We have detailed how to improve compatibility with the tidyverse by explaining: -- [how functions taking data.frames or data.frames subclass can easily and should also allow compatibility with tibble](https://hugogruson.fr/posts/compa-tibble/) -- [how to ensure class attribute is preserved whenever possible while using dplyr functions](../extend-dataframes). +- [how functions taking data.frames or data.frames subclass should also allow compatibility with tibble, which can be done in a few steps](https://hugogruson.fr/posts/compa-tibble/) +- [how to ensure class attributes are preserved whenever possible while using dplyr functions](../extend-dataframes). -Here, we are going to explore how to actually start adding support in the ecosystem for the newly S3 classes while minimizing user-facing breaking changes. -We have previously delved into this topic with our previous post ["Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations"](../s3-generic) and this is a wider and higher-level view of the same topic. +Here, we are going to explore how to start adding support in the ecosystem for the new S3 classes while minimizing user-facing breaking changes. +We have previously delved into this topic with our post ["Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations"](../s3-generic) and this is a wider and higher-level view of the same topic. The strategy presented here is the declination of a common concept in web development and the web ecosystem: [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). This philosophy aims to support browsers with limited range of features, while allowing a slightly richer experience for browsers with extra features. It makes sense to think about this philosophy with the prism of introducing new classes to a new software ecosystem as it has the similar constraints of multiple stakeholders with different interests and timelines. @@ -34,7 +34,7 @@ This can conveniently be done in an almost transparent way by converting the old ![A before / after type image showing the conversion of a function to a generic with a default method keeping the exisiting behaviour.](convert_to_generic.svg) -If the function was already a generic, even steps are required: a new method for the new class should be added, leaving everything else unchanged. +If the function was already a generic, then a new method for the new class should be added, leaving everything else unchanged. ## Adding class support to function outputs via progressive enhancement From 6679ed6438e5eb344f71b6b70b6e5b4186ac220a Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:44:09 +0200 Subject: [PATCH 06/14] Add more concrete examples --- posts/progressive-enhancement/index.qmd | 95 +++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index 2ff7fee7..e6987b94 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -34,6 +34,61 @@ This can conveniently be done in an almost transparent way by converting the old ![A before / after type image showing the conversion of a function to a generic with a default method keeping the exisiting behaviour.](convert_to_generic.svg) +For a different, additional, example, we can consider a function working on patient-level data, which previously only accepted a `data.frame` as input: + +```{r} +#' Compute length of stay in hospital on a patient-level dataset +#' +#' @param data A data.frame containing patient-level data +#' @param admission_column The name of the column containing the admission date +#' @param discharge_column The name of the column containing the discharge date +#' +#' @returns A numeric vector of hospitalization durations in days +compute_hospitalization_duration <- function(data, admission_column, discharge_column) { + + difftime( + data[[discharge_column]], + data[[admission_column]], + units = "days" + ) + +} +``` + +We want to add support for `linelist` objects, as defined in the [linelist package](https://epiverse-trace.github.io/linelist). `linelist` objects inherit from `data.frame` and contain an additional `tags` attribute. In particular, `linelist` objects can have a `date_admission` and `date_discharge` tag. This means we can use the tags to automatically detect the columns to use. + +But we want the function to keep working for standard `data.frame`s, `tibble`s, etc. We can follow the steps described in the previous post to convert the function to a generic, and add a default method to handle the old behaviour: + +```{r} +compute_hospitalization_duration <- function(data, ...) { + + UseMethod("compute_hospitalization_duration") + +} + +compute_hospitalization_duration.default <- function(data, admission_column, discharge_column) { + + difftime( + data[[discharge_column]], + data[[admission_column]], + units = "days" + ) + +} + +compute_hospitalization_duration.linelist <- function(data, ...) { + + x <- linelist::tags_df(data) + + compute_hospitalization_duration( + data = x, + admission_column = "date_admission", + discharge_column = "date_discharge" + ) + +} +``` + If the function was already a generic, then a new method for the new class should be added, leaving everything else unchanged. ## Adding class support to function outputs via progressive enhancement @@ -46,6 +101,46 @@ While the new argument approach is sometimes indeed the only possible method, th In particular, this is the case when the old output was already inheriting from the parent of the new class (hence the importance of carefully choosing the parent class). In this situation, the new attributes from the new class should not interfere with existing code for downstream analysis. +In this case, let's consider a function that was previously returning an unclassed `data.frame` with patient-level data: + +```{r} +create_patient_dataset <- function(n_patients = 10) { + + data <- data.frame( + patient_id = seq_len(n_patients), + age = sample(18:99, n_patients, replace = TRUE) + ) + + return(data) + +} +``` + +We want to start returning a `linelist` object. Because `linelist` objects are `data.frame`s (or `tibble`s) with an extra `attr`, it can be done in a transparent way: + +```{r} +create_patient_dataset <- function(n_patients = 10) { + + data <- data.frame( + patient_id = seq_len(n_patients), + age = sample(18:99, n_patients, replace = TRUE) + ) + + data <- linelist::make_linelist( + data, + id = "patient_id", + age = "age" + ) + + return(data) + +} + +inherits(data, "data.frame") +``` + +For a more realistic example, you can also see the work in progress to integrate the [new `contactmatrix` standard format](https://github.com/socialcontactdata/contactmatrix) for social contact data to the [contactdata package](https://github.com/Bisaloo/contactdata). + This is however only true if code in downstream analysis follows good practices [^1]. If existing code was testing equality of the class to a certain value, it will break when the new class value is appended. This is described in a [post on the R developer blog, when base R was adding a new `array` class value to `matrix` objects](https://developer.r-project.org/Blog/public/2019/11/09/when-you-think-class.-think-again/index.html). Class inheritance should never be tested via `class(x) == "some_class"`. Instead, `inherits(x, "some_class")` or `is(x, "some_class")` should be used to future-proof the code and allow appending an additional in the future. [^1]: This is now [enforced in R packages by R CMD check](https://github.com/r-devel/r-svn/commit/77ebdff5adc200dfe9bc850bc4447088830d2ee0), and via the [`class_equals_linter()`](https://lintr.r-lib.org/reference/class_equals_linter.html) in the [lintr package](https://lintr.r-lib.org/). From 222b416811719623dbc8fe5cb267d83da89dc001 Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:44:33 +0200 Subject: [PATCH 07/14] Rephrase "declination" --- posts/progressive-enhancement/index.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index e6987b94..626d390d 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -22,7 +22,7 @@ We have detailed how to improve compatibility with the tidyverse by explaining: Here, we are going to explore how to start adding support in the ecosystem for the new S3 classes while minimizing user-facing breaking changes. We have previously delved into this topic with our post ["Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations"](../s3-generic) and this is a wider and higher-level view of the same topic. -The strategy presented here is the declination of a common concept in web development and the web ecosystem: [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). This philosophy aims to support browsers with limited range of features, while allowing a slightly richer experience for browsers with extra features. +The strategy presented here is the variation of a common concept in web development and the web ecosystem: [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). This philosophy aims to support browsers with limited range of features, while allowing a slightly richer experience for browsers with extra features. It makes sense to think about this philosophy with the prism of introducing new classes to a new software ecosystem as it has the similar constraints of multiple stakeholders with different interests and timelines. The application of progressive enhancement in this context means that users or packages that did not (yet) adopt the new classes are not penalized compared to users or packages that did. From c222a944537edf012820f67512a88c82b4505b3b Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:48:18 +0200 Subject: [PATCH 08/14] Commit _freeze --- .../index/execute-results/html.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 _freeze/posts/progressive-enhancement/index/execute-results/html.json diff --git a/_freeze/posts/progressive-enhancement/index/execute-results/html.json b/_freeze/posts/progressive-enhancement/index/execute-results/html.json new file mode 100644 index 00000000..43588e86 --- /dev/null +++ b/_freeze/posts/progressive-enhancement/index/execute-results/html.json @@ -0,0 +1,15 @@ +{ + "hash": "2fde11041aff189be02b5d60b3f477ed", + "result": { + "engine": "knitr", + "markdown": "---\ntitle: \"Improving Ecosystem Interoperability Iteratively via Progressive Enhancement\"\nauthor:\n - name: \"Hugo Gruson\"\n orcid: \"0000-0002-4094-1476\"\ndate: \"2024-07-04\"\ncategories: [R, interoperability, S3, progressive enhancement, ecosystem, lifecycle, object orientation, DOI]\nformat:\n html: \n toc: true\n---\n\n\nWe are continuing our post series on S3 and interoperability.\nWe have previously discussed [what makes a good S3 class and how to choose a good parent for it, as well as when to write or not write a custom method](../parent-class).\nWe have highlighted in particular how classes inheriting from data.frames can simplify user experience because of familiarity, and reduce developer workload due to the pre-existing S3 methods.\n\nWe have detailed how to improve compatibility with the tidyverse by explaining:\n\n- [how functions taking data.frames or data.frames subclass should also allow compatibility with tibble, which can be done in a few steps](https://hugogruson.fr/posts/compa-tibble/)\n- [how to ensure class attributes are preserved whenever possible while using dplyr functions](../extend-dataframes).\n\nHere, we are going to explore how to start adding support in the ecosystem for the new S3 classes while minimizing user-facing breaking changes.\nWe have previously delved into this topic with our post [\"Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations\"](../s3-generic) and this is a wider and higher-level view of the same topic.\n\nThe strategy presented here is the variation of a common concept in web development and the web ecosystem: [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). This philosophy aims to support browsers with limited range of features, while allowing a slightly richer experience for browsers with extra features.\nIt makes sense to think about this philosophy with the prism of introducing new classes to a new software ecosystem as it has the similar constraints of multiple stakeholders with different interests and timelines.\nThe application of progressive enhancement in this context means that users or packages that did not (yet) adopt the new classes are not penalized compared to users or packages that did.\n\n## Adding class support to function inputs via progressive enhancement\n\nThe goal here is to allow functions to accept the new classes as inputs, while keeping the old behaviour unchanged for unclassed objects (or with a different class than the new one).\n\nThis can conveniently be done in an almost transparent way by converting the old function to the S3 generic, and using the default method to handle the old behaviour. The practical steps, and minor caveats, have been previously described in the post [\"Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations\"](../s3-generic).\n\n![A before / after type image showing the conversion of a function to a generic with a default method keeping the exisiting behaviour.](convert_to_generic.svg)\n\nFor a different, additional, example, we can consider a function working on patient-level data, which previously only accepted a `data.frame` as input:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n#' Compute length of stay in hospital on a patient-level dataset\n#'\n#' @param data A data.frame containing patient-level data\n#' @param admission_column The name of the column containing the admission date\n#' @param discharge_column The name of the column containing the discharge date\n#'\n#' @returns A numeric vector of hospitalization durations in days\ncompute_hospitalization_duration <- function(data, admission_column, discharge_column) {\n\n difftime(\n data[[discharge_column]],\n data[[admission_column]],\n units = \"days\"\n )\n\n}\n```\n:::\n\n\nWe want to add support for `linelist` objects, as defined in the [linelist package](https://epiverse-trace.github.io/linelist). `linelist` objects inherit from `data.frame` and contain an additional `tags` attribute. In particular, `linelist` objects can have a `date_admission` and `date_discharge` tag. This means we can use the tags to automatically detect the columns to use.\n\nBut we want the function to keep working for standard `data.frame`s, `tibble`s, etc. We can follow the steps described in the previous post to convert the function to a generic, and add a default method to handle the old behaviour:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncompute_hospitalization_duration <- function(data, ...) {\n\n UseMethod(\"compute_hospitalization_duration\")\n\n}\n\ncompute_hospitalization_duration.default <- function(data, admission_column, discharge_column) {\n\n difftime(\n data[[discharge_column]],\n data[[admission_column]],\n units = \"days\"\n )\n\n}\n\ncompute_hospitalization_duration.linelist <- function(data, ...) {\n\n x <- linelist::tags_df(data)\n\n compute_hospitalization_duration(\n data = x,\n admission_column = \"date_admission\",\n discharge_column = \"date_discharge\"\n )\n\n}\n```\n:::\n\n\nIf the function was already a generic, then a new method for the new class should be added, leaving everything else unchanged.\n\n## Adding class support to function outputs via progressive enhancement\n\nAdding class support to function outputs is often more challenging.\nA common option is to add a new argument to the function, which would be a boolean indicating whether the output should be of the new class or not.\nBut this doesn't fit in the view of progressive enhancement, as it would require users to change their code to benefit from the new classes, or to suffer from breaking changes.\n\nWhile the new argument approach is sometimes indeed the only possible method, there are some situations where we can have an approach truly following the progressive enhancement philosophy.\n\nIn particular, this is the case when the old output was already inheriting from the parent of the new class (hence the importance of carefully choosing the parent class). In this situation, the new attributes from the new class should not interfere with existing code for downstream analysis.\n\nIn this case, let's consider a function that was previously returning an unclassed `data.frame` with patient-level data:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncreate_patient_dataset <- function(n_patients = 10) {\n\n data <- data.frame(\n patient_id = seq_len(n_patients),\n age = sample(18:99, n_patients, replace = TRUE)\n )\n\n return(data)\n\n}\n```\n:::\n\n\nWe want to start returning a `linelist` object. Because `linelist` objects are `data.frame`s (or `tibble`s) with an extra `attr`, it can be done in a transparent way:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncreate_patient_dataset <- function(n_patients = 10) {\n\n data <- data.frame(\n patient_id = seq_len(n_patients),\n age = sample(18:99, n_patients, replace = TRUE)\n )\n\n data <- linelist::make_linelist(\n data,\n id = \"patient_id\",\n age = \"age\"\n )\n\n return(data)\n\n}\n\ninherits(data, \"data.frame\")\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] FALSE\n```\n\n\n:::\n:::\n\n\nFor a more realistic example, you can also see the work in progress to integrate the [new `contactmatrix` standard format](https://github.com/socialcontactdata/contactmatrix) for social contact data to the [contactdata package](https://github.com/Bisaloo/contactdata).\n\nThis is however only true if code in downstream analysis follows good practices [^1]. If existing code was testing equality of the class to a certain value, it will break when the new class value is appended. This is described in a [post on the R developer blog, when base R was adding a new `array` class value to `matrix` objects](https://developer.r-project.org/Blog/public/2019/11/09/when-you-think-class.-think-again/index.html). Class inheritance should never be tested via `class(x) == \"some_class\"`. Instead, `inherits(x, \"some_class\")` or `is(x, \"some_class\")` should be used to future-proof the code and allow appending an additional in the future.\n\n[^1]: This is now [enforced in R packages by R CMD check](https://github.com/r-devel/r-svn/commit/77ebdff5adc200dfe9bc850bc4447088830d2ee0), and via the [`class_equals_linter()`](https://lintr.r-lib.org/reference/class_equals_linter.html) in the [lintr package](https://lintr.r-lib.org/).\n\n## Conclusion\n\nObject oriented programming and S3 classes offer a convenient way to iteratively add interoperability in the ecosystem in a way that is minimally disruptive to users and developers.\nNewly classed input support can be added via custom methods (after converting the existing function to a generic if necessary).\nNewly classed output support can be added via progressive enhancement, by ensuring that the new class is a subclass of the old one, that downstream code uses good practices.\n", + "supporting": [], + "filters": [ + "rmarkdown/pagebreak.lua" + ], + "includes": {}, + "engineDependencies": {}, + "preserve": {}, + "postProcess": true + } +} \ No newline at end of file From e65f0f5544e13e2f10a2f3d60c135fc1870ab2fa Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:06:07 +0000 Subject: [PATCH 09/14] Rephrase based on James' suggestion Co-authored-by: James Azam --- posts/progressive-enhancement/index.qmd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index 626d390d..c914f49f 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -10,7 +10,7 @@ format: toc: true --- -We are continuing our post series on S3 and interoperability. +We are continuing our post series on S3 object orientation and interoperability in R. We have previously discussed [what makes a good S3 class and how to choose a good parent for it, as well as when to write or not write a custom method](../parent-class). We have highlighted in particular how classes inheriting from data.frames can simplify user experience because of familiarity, and reduce developer workload due to the pre-existing S3 methods. @@ -22,9 +22,9 @@ We have detailed how to improve compatibility with the tidyverse by explaining: Here, we are going to explore how to start adding support in the ecosystem for the new S3 classes while minimizing user-facing breaking changes. We have previously delved into this topic with our post ["Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations"](../s3-generic) and this is a wider and higher-level view of the same topic. -The strategy presented here is the variation of a common concept in web development and the web ecosystem: [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). This philosophy aims to support browsers with limited range of features, while allowing a slightly richer experience for browsers with extra features. +The strategy presented here is the variation of a common concept in web development and the web ecosystem: [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). This philosophy aims to support browsers with a common set of essential features, and even richer features for browser with the most recent updates. It makes sense to think about this philosophy with the prism of introducing new classes to a new software ecosystem as it has the similar constraints of multiple stakeholders with different interests and timelines. -The application of progressive enhancement in this context means that users or packages that did not (yet) adopt the new classes are not penalized compared to users or packages that did. +The application of progressive enhancement in this context means that users or packages that have not (yet) adopted the new classes are not penalized compared to users or packages that have. ## Adding class support to function inputs via progressive enhancement @@ -141,7 +141,7 @@ inherits(data, "data.frame") For a more realistic example, you can also see the work in progress to integrate the [new `contactmatrix` standard format](https://github.com/socialcontactdata/contactmatrix) for social contact data to the [contactdata package](https://github.com/Bisaloo/contactdata). -This is however only true if code in downstream analysis follows good practices [^1]. If existing code was testing equality of the class to a certain value, it will break when the new class value is appended. This is described in a [post on the R developer blog, when base R was adding a new `array` class value to `matrix` objects](https://developer.r-project.org/Blog/public/2019/11/09/when-you-think-class.-think-again/index.html). Class inheritance should never be tested via `class(x) == "some_class"`. Instead, `inherits(x, "some_class")` or `is(x, "some_class")` should be used to future-proof the code and allow appending an additional in the future. +This is however only true if code in downstream analysis follows good practices in checking for the class of an object [^1]. If existing code was testing equality of the class to a certain value, it will break when the new class value is appended. This is described in a [post on the R developer blog, when base R was adding a new `array` class value to `matrix` objects](https://developer.r-project.org/Blog/public/2019/11/09/when-you-think-class.-think-again/index.html). Class inheritance should never be tested via `class(x) == "some_class"`. Instead, `inherits(x, "some_class")` or `is(x, "some_class")` should be used to future-proof the code and allow appending an additional in the future. [^1]: This is now [enforced in R packages by R CMD check](https://github.com/r-devel/r-svn/commit/77ebdff5adc200dfe9bc850bc4447088830d2ee0), and via the [`class_equals_linter()`](https://lintr.r-lib.org/reference/class_equals_linter.html) in the [lintr package](https://lintr.r-lib.org/). From bc739df5dd730196c2dc107b64169dd1d52b3095 Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:34:26 +0200 Subject: [PATCH 10/14] Clarify last sentence --- posts/progressive-enhancement/index.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index c914f49f..376b4d72 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -149,4 +149,4 @@ This is however only true if code in downstream analysis follows good practices Object oriented programming and S3 classes offer a convenient way to iteratively add interoperability in the ecosystem in a way that is minimally disruptive to users and developers. Newly classed input support can be added via custom methods (after converting the existing function to a generic if necessary). -Newly classed output support can be added via progressive enhancement, by ensuring that the new class is a subclass of the old one, that downstream code uses good practices. +Newly classed output support can be added via progressive enhancement, by ensuring that the new class is a subclass of the old one and that downstream code uses good practices to test class inheritance. From 3e52c7b961128165ebad1056e438d5422ef46e2d Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:34:50 +0200 Subject: [PATCH 11/14] Update date --- posts/progressive-enhancement/index.qmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index 376b4d72..3671ce46 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -3,7 +3,7 @@ title: "Improving Ecosystem Interoperability Iteratively via Progressive Enhance author: - name: "Hugo Gruson" orcid: "0000-0002-4094-1476" -date: "2024-07-04" +date: "2024-07-05" categories: [R, interoperability, S3, progressive enhancement, ecosystem, lifecycle, object orientation, DOI] format: html: From 6de49ce9906efcf4898526b379827d9119063cd9 Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:35:27 +0200 Subject: [PATCH 12/14] Convert code blocks to markup --- posts/progressive-enhancement/index.qmd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index 3671ce46..96c5b584 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -36,7 +36,7 @@ This can conveniently be done in an almost transparent way by converting the old For a different, additional, example, we can consider a function working on patient-level data, which previously only accepted a `data.frame` as input: -```{r} +```r #' Compute length of stay in hospital on a patient-level dataset #' #' @param data A data.frame containing patient-level data @@ -59,7 +59,7 @@ We want to add support for `linelist` objects, as defined in the [linelist packa But we want the function to keep working for standard `data.frame`s, `tibble`s, etc. We can follow the steps described in the previous post to convert the function to a generic, and add a default method to handle the old behaviour: -```{r} +```r compute_hospitalization_duration <- function(data, ...) { UseMethod("compute_hospitalization_duration") @@ -103,7 +103,7 @@ In particular, this is the case when the old output was already inheriting from In this case, let's consider a function that was previously returning an unclassed `data.frame` with patient-level data: -```{r} +```r create_patient_dataset <- function(n_patients = 10) { data <- data.frame( @@ -118,7 +118,7 @@ create_patient_dataset <- function(n_patients = 10) { We want to start returning a `linelist` object. Because `linelist` objects are `data.frame`s (or `tibble`s) with an extra `attr`, it can be done in a transparent way: -```{r} +```r create_patient_dataset <- function(n_patients = 10) { data <- data.frame( From f8590cc6bb469b2a40e75e08829763a7b465f402 Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:35:44 +0200 Subject: [PATCH 13/14] Remove _freeze/ --- .../index/execute-results/html.json | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 _freeze/posts/progressive-enhancement/index/execute-results/html.json diff --git a/_freeze/posts/progressive-enhancement/index/execute-results/html.json b/_freeze/posts/progressive-enhancement/index/execute-results/html.json deleted file mode 100644 index 43588e86..00000000 --- a/_freeze/posts/progressive-enhancement/index/execute-results/html.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hash": "2fde11041aff189be02b5d60b3f477ed", - "result": { - "engine": "knitr", - "markdown": "---\ntitle: \"Improving Ecosystem Interoperability Iteratively via Progressive Enhancement\"\nauthor:\n - name: \"Hugo Gruson\"\n orcid: \"0000-0002-4094-1476\"\ndate: \"2024-07-04\"\ncategories: [R, interoperability, S3, progressive enhancement, ecosystem, lifecycle, object orientation, DOI]\nformat:\n html: \n toc: true\n---\n\n\nWe are continuing our post series on S3 and interoperability.\nWe have previously discussed [what makes a good S3 class and how to choose a good parent for it, as well as when to write or not write a custom method](../parent-class).\nWe have highlighted in particular how classes inheriting from data.frames can simplify user experience because of familiarity, and reduce developer workload due to the pre-existing S3 methods.\n\nWe have detailed how to improve compatibility with the tidyverse by explaining:\n\n- [how functions taking data.frames or data.frames subclass should also allow compatibility with tibble, which can be done in a few steps](https://hugogruson.fr/posts/compa-tibble/)\n- [how to ensure class attributes are preserved whenever possible while using dplyr functions](../extend-dataframes).\n\nHere, we are going to explore how to start adding support in the ecosystem for the new S3 classes while minimizing user-facing breaking changes.\nWe have previously delved into this topic with our post [\"Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations\"](../s3-generic) and this is a wider and higher-level view of the same topic.\n\nThe strategy presented here is the variation of a common concept in web development and the web ecosystem: [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). This philosophy aims to support browsers with limited range of features, while allowing a slightly richer experience for browsers with extra features.\nIt makes sense to think about this philosophy with the prism of introducing new classes to a new software ecosystem as it has the similar constraints of multiple stakeholders with different interests and timelines.\nThe application of progressive enhancement in this context means that users or packages that did not (yet) adopt the new classes are not penalized compared to users or packages that did.\n\n## Adding class support to function inputs via progressive enhancement\n\nThe goal here is to allow functions to accept the new classes as inputs, while keeping the old behaviour unchanged for unclassed objects (or with a different class than the new one).\n\nThis can conveniently be done in an almost transparent way by converting the old function to the S3 generic, and using the default method to handle the old behaviour. The practical steps, and minor caveats, have been previously described in the post [\"Convert Your R Function to an S3 Generic: Benefits, Pitfalls & Design Considerations\"](../s3-generic).\n\n![A before / after type image showing the conversion of a function to a generic with a default method keeping the exisiting behaviour.](convert_to_generic.svg)\n\nFor a different, additional, example, we can consider a function working on patient-level data, which previously only accepted a `data.frame` as input:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n#' Compute length of stay in hospital on a patient-level dataset\n#'\n#' @param data A data.frame containing patient-level data\n#' @param admission_column The name of the column containing the admission date\n#' @param discharge_column The name of the column containing the discharge date\n#'\n#' @returns A numeric vector of hospitalization durations in days\ncompute_hospitalization_duration <- function(data, admission_column, discharge_column) {\n\n difftime(\n data[[discharge_column]],\n data[[admission_column]],\n units = \"days\"\n )\n\n}\n```\n:::\n\n\nWe want to add support for `linelist` objects, as defined in the [linelist package](https://epiverse-trace.github.io/linelist). `linelist` objects inherit from `data.frame` and contain an additional `tags` attribute. In particular, `linelist` objects can have a `date_admission` and `date_discharge` tag. This means we can use the tags to automatically detect the columns to use.\n\nBut we want the function to keep working for standard `data.frame`s, `tibble`s, etc. We can follow the steps described in the previous post to convert the function to a generic, and add a default method to handle the old behaviour:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncompute_hospitalization_duration <- function(data, ...) {\n\n UseMethod(\"compute_hospitalization_duration\")\n\n}\n\ncompute_hospitalization_duration.default <- function(data, admission_column, discharge_column) {\n\n difftime(\n data[[discharge_column]],\n data[[admission_column]],\n units = \"days\"\n )\n\n}\n\ncompute_hospitalization_duration.linelist <- function(data, ...) {\n\n x <- linelist::tags_df(data)\n\n compute_hospitalization_duration(\n data = x,\n admission_column = \"date_admission\",\n discharge_column = \"date_discharge\"\n )\n\n}\n```\n:::\n\n\nIf the function was already a generic, then a new method for the new class should be added, leaving everything else unchanged.\n\n## Adding class support to function outputs via progressive enhancement\n\nAdding class support to function outputs is often more challenging.\nA common option is to add a new argument to the function, which would be a boolean indicating whether the output should be of the new class or not.\nBut this doesn't fit in the view of progressive enhancement, as it would require users to change their code to benefit from the new classes, or to suffer from breaking changes.\n\nWhile the new argument approach is sometimes indeed the only possible method, there are some situations where we can have an approach truly following the progressive enhancement philosophy.\n\nIn particular, this is the case when the old output was already inheriting from the parent of the new class (hence the importance of carefully choosing the parent class). In this situation, the new attributes from the new class should not interfere with existing code for downstream analysis.\n\nIn this case, let's consider a function that was previously returning an unclassed `data.frame` with patient-level data:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncreate_patient_dataset <- function(n_patients = 10) {\n\n data <- data.frame(\n patient_id = seq_len(n_patients),\n age = sample(18:99, n_patients, replace = TRUE)\n )\n\n return(data)\n\n}\n```\n:::\n\n\nWe want to start returning a `linelist` object. Because `linelist` objects are `data.frame`s (or `tibble`s) with an extra `attr`, it can be done in a transparent way:\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncreate_patient_dataset <- function(n_patients = 10) {\n\n data <- data.frame(\n patient_id = seq_len(n_patients),\n age = sample(18:99, n_patients, replace = TRUE)\n )\n\n data <- linelist::make_linelist(\n data,\n id = \"patient_id\",\n age = \"age\"\n )\n\n return(data)\n\n}\n\ninherits(data, \"data.frame\")\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] FALSE\n```\n\n\n:::\n:::\n\n\nFor a more realistic example, you can also see the work in progress to integrate the [new `contactmatrix` standard format](https://github.com/socialcontactdata/contactmatrix) for social contact data to the [contactdata package](https://github.com/Bisaloo/contactdata).\n\nThis is however only true if code in downstream analysis follows good practices [^1]. If existing code was testing equality of the class to a certain value, it will break when the new class value is appended. This is described in a [post on the R developer blog, when base R was adding a new `array` class value to `matrix` objects](https://developer.r-project.org/Blog/public/2019/11/09/when-you-think-class.-think-again/index.html). Class inheritance should never be tested via `class(x) == \"some_class\"`. Instead, `inherits(x, \"some_class\")` or `is(x, \"some_class\")` should be used to future-proof the code and allow appending an additional in the future.\n\n[^1]: This is now [enforced in R packages by R CMD check](https://github.com/r-devel/r-svn/commit/77ebdff5adc200dfe9bc850bc4447088830d2ee0), and via the [`class_equals_linter()`](https://lintr.r-lib.org/reference/class_equals_linter.html) in the [lintr package](https://lintr.r-lib.org/).\n\n## Conclusion\n\nObject oriented programming and S3 classes offer a convenient way to iteratively add interoperability in the ecosystem in a way that is minimally disruptive to users and developers.\nNewly classed input support can be added via custom methods (after converting the existing function to a generic if necessary).\nNewly classed output support can be added via progressive enhancement, by ensuring that the new class is a subclass of the old one, that downstream code uses good practices.\n", - "supporting": [], - "filters": [ - "rmarkdown/pagebreak.lua" - ], - "includes": {}, - "engineDependencies": {}, - "preserve": {}, - "postProcess": true - } -} \ No newline at end of file From 2778702d3a9d437de2829779ac479326c0b3a652 Mon Sep 17 00:00:00 2001 From: Hugo Gruson <10783929+Bisaloo@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:38:19 +0200 Subject: [PATCH 14/14] Acknowledge reviewers --- posts/progressive-enhancement/index.qmd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/posts/progressive-enhancement/index.qmd b/posts/progressive-enhancement/index.qmd index 96c5b584..86fd20ac 100644 --- a/posts/progressive-enhancement/index.qmd +++ b/posts/progressive-enhancement/index.qmd @@ -150,3 +150,5 @@ This is however only true if code in downstream analysis follows good practices Object oriented programming and S3 classes offer a convenient way to iteratively add interoperability in the ecosystem in a way that is minimally disruptive to users and developers. Newly classed input support can be added via custom methods (after converting the existing function to a generic if necessary). Newly classed output support can be added via progressive enhancement, by ensuring that the new class is a subclass of the old one and that downstream code uses good practices to test class inheritance. + +**Thanks to James Azam and Tim Taylor for their very valuable feedback on this post.**