Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FR: allow flow_view to parse "withr" or "suppress" family of functions #162

Open
zkamvar opened this issue Jul 10, 2023 · 3 comments
Open

Comments

@zkamvar
Copy link
Contributor

zkamvar commented Jul 10, 2023

I notice that if I have a {withr}-like pattern in my code or use a suppress family function, {flow} cannot disentangle it.

I will occasionally have functions that look like this:

fun <- function(path, ...) {
  # ... 
  withr::with_dir(path, {
    # ... 
  })
  # ...
}

When flow encounters the {withr} call, it does not parse the expression. The same is true for calls to suppressMessages(). As a reproducible example, here is something should produce a simple if/else flow chart:

f <- function() {
  suppressMessages({
    if (getOption("a.test") == 1L) print("1") else print("2")
})
}

flow::flow_view(f)

flowchart with three nodes: the function call, the function body, and the exit state. No if else logic is in there.

Created on 2023-07-10 with reprex v2.0.2

That being said, I really like {flow}! I'm in the process of onboarding new developers and being able to create these visuals to walk through the logic is super helpful!

@moodymudskipper
Copy link
Owner

moodymudskipper commented Jul 11, 2023

I like the idea but I wonder what this would look like and how it would work.

Functions are common offenders for this situation and I implemented the nested_fun argument for those.

We might do something like this :

filename

But then what do we do with : expression() or tryCatch() that might nest several of those in a single call?

expression(if(...) ..., if(...) ...)
tryCatch(if(...) ..., error = function(e) if(...) ...)

Maybe this would be a more general way, so if we have a several nested inputs they would be called 1*, 1** etc:

filename-4

Or the other way around

filename-5

I think I like the latter better because it reads from top to bottom more naturally.

The rule would probably be that whenever control flow is found in an argument the full argument is expanded like this. function definitions might not need special casing.

In few cases when we have deeper nesting of those this might still work, we'd have another box in the old box, and if we use index as in examples above the box indices don't change compared to current situation so this might even be tackled as an ulterior step, so we don't have to dig into the algorithm too much.

Then we'd have to choose if we make it optional, but probably have it work always like this would be fine.

@zkamvar How do these proposals above look ?

@zkamvar
Copy link
Contributor Author

zkamvar commented Jul 12, 2023

Thank you for the quick reply! I agree with your assessment and am very much a fan of the latter proposal---the top-down arrangement makes it much easier to read and the box with the starred index gives is just the right amount of context for me to know what's going on.

@moodymudskipper
Copy link
Owner

Thanks. This has good value I think so should be a priority in this package, but I don't have a lot of time for it these days so it'll have to wait a bit.

Tests cases should be the given example first, then bquote() because it has 2 functions to expand in the same block, and one function that we don't need to expand :

flow::flow_view(bquote)
#> We found function definitions in this code, use the argument nested_fun to inspect them
#> $unquote
#> function(e) {
#>     if (is.pairlist(e)) 
#>         as.pairlist(lapply(e, unquote))
#>     else if (is.call(e)) {
#>         if (is.name(e[[1L]]) && as.character(e[[1]]) == ".") 
#>             eval(e[[2L]], where)
#>         else if (splice) {
#>             if (is.name(e[[1L]]) && as.character(e[[1L]]) == 
#>                 "..") 
#>                 stop("can only splice inside a call", call. = FALSE)
#>             else as.call(unquote.list(e))
#>         }
#>         else as.call(lapply(e, unquote))
#>     }
#>     else e
#> }
#> 
#> $is.splice.macro
#> function(e) is.call(e) && is.name(e[[1L]]) && as.character(e[[1L]]) == 
#>     ".."
#> 
#> $unquote.list
#> function(e) {
#>     p <- Position(is.splice.macro, e, nomatch = NULL)
#>     if (is.null(p)) 
#>         lapply(e, unquote)
#>     else {
#>         n <- length(e)
#>         head <- if (p == 1) 
#>             NULL
#>         else e[1:(p - 1)]
#>         tail <- if (p == n) 
#>             NULL
#>         else e[(p + 1):n]
#>         macro <- e[[p]]
#>         mexp <- eval(macro[[2L]], where)
#>         if (!is.vector(mexp) && !is.expression(mexp)) 
#>             stop("can only splice vectors")
#>         c(lapply(head, unquote), mexp, as.list(unquote.list(tail)))
#>     }
#> }

Created on 2023-07-12 with reprex v2.0.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants