R Client User Interface Design

R Client User Interface Design

To provide better user experience with synapser, several interface patterns are being considered for exposing model class methods in the Synapse R client, including entities such as File, Project, and Table. The goal is to identify the design that is most natural and idiomatic for R users.

These proposals focus only on model-specific methods. High-level CRUD operations such as get, store, and delete would continue to dispatch automatically to the appropriate entity type, so users would not need to call entity-specific CRUD methods directly.

Decision Framing

What is being decided: How model class methods should be exposed in the Synapse R client.

Why this matters: This choice will affect usability, consistency, discoverability, documentation quality, and how well the API aligns with common R conventions.

Evaluation Criteria

  • Idiomatic for R users

  • Easy to discover via tab completion, hovering, and documentation

  • Clear separation of constructor arguments and method arguments

  • Scales well as entities gain more fields and methods

  • Supports readable workflows for common operations

Options Considered

Option

Summary

Strengths

Risks / Drawbacks

Overall Assessment

Option

Summary

Strengths

Risks / Drawbacks

Overall Assessment

Instance-First with Pipe

User constructs or retrieves an object and pass it through the pipe and applying methods directly to it

  1. Aligns with tidyverse workflows;

  2. Short function signatures;

  3. Supports generic method name, e.g. synGetAcl instead of synGetAclProject

Best fit may depend on how broadly pipe-oriented workflows are expected across the client.

Most promising balance of clarity, scalability, and R ergonomics.

Explicit Instance Parameter

User constructs an object first, then passes it as the first positional argument to the method.

  1. Familiar object-oriented pattern;

  2. Close to S3 method conventions.

  3. Clear entity specific function method, such as synGetAclProject

  1. Requires explicit object construction;

  2. May feel heavier than necessary for some users;

  3. Duplicate function names, such as synGetAclProject, synGetAclFolder

Reasonable and conventional, but somewhat verbose.

Fully Flattened Functional Interface

Generated function constructs the object internally; user passes only raw arguments.

  1. Simple for straightforward cases;

  2. Avoids explicit object creation.

Large signatures; constructor and method parameters get mixed together; weaker discoverability.

Easy at first glance, but likely hard to scale cleanly.

Detailed Option Analysis

Option 1: Instance-First with Pipe

In this approach, the object is passed through the pipe and methods operate on it. This mirrors tidyverse conventions, where data flows left to right and each verb declares only its own parameters.

Reference: purrr documentation

Example: version a table

Table(id = "syn1234") |> synSnapshot(comment = "test_2_pipe")

Example: query a table

synQuery(query = "SELECT * FROM syn1234")

Example: update table annotations with a helper

synGet(synapse_id = "syn1234") |> synSetProperties( annotations = list( first_annotation = "value", second_annotation = "value2" ) ) |> synStore()

Example: upload a file

synStore( File( path = "...", parent_id = "syn1234" ) )

Alternative upload form

File( path = "...", parent_id = "syn1234" ) |> synStore()

Example: download a file

synGet( synapse_id = "syn1234", path = "...", file_options = FileOptions(download_file = TRUE) )

Alternative download form

File( id = "syn1234", path = "..." ) |> synGet( file_options = FileOptions(download_file = TRUE) )

Pros

  • Aligns with tidyverse and pipe-oriented workflows.

  • Keeps function signatures short and discoverable.

  • Constructor parameters remain separate from method parameters.

  • Feels natural to many R users already familiar with %>% and |>.

  • Scales effectively as more methods or models are added.

Additional benefit

If we adopt this pattern, we don’t need entity-specific methods such as synSnapshotTable() and synSnapshotDataset(). Because the entity type is already known from the constructed object, a generic synSnapshot() could potentially handle all supported entity types.

Option 2: Explicit Instance Parameter

In this approach, the user first constructs an object and then passes it as an argument to the method. This is similar to R's S3 method style, where the model object is passed first and dispatch is based on class.

Reference: predict.lm documentation

Example: version a table

synSnapshotTable( instance = Table(id = "syn1234"), comment = "test_2_pipe" )

Example: update table annotations

table <- synGet(synapse_id = "syn1234") table$annotations <- list( first_annotation = "value", second_annotation = "value2" ) synStore(entity = table)

Example: upload a file

synStore( entity = File( path = "...", parent_id = "syn1234" ) )

Example: download a file

synGet( synapse_id = "syn1234", path = "...", file_options = FileOptions(download_file = TRUE) )

Pros

  • Familiar object-oriented pattern.

  • Similar to existing S3 method conventions.

Cons

  • Requires users to explicitly construct objects before calling methods.

  • May feel less natural for some R users.

Option 3: Fully Flattened Functional Interface

In this approach, the generated function constructs the object internally and then invokes the method. Users never interact directly with the underlying object instance.

Example

synSnapshotTable( id = "syn1234", comment = "test" )

Illustrative expanded signature

synSnapshotTable( id = NULL, name = NULL, parent_id = NULL, activity = NULL, ... comment = NULL, timeout = 120L, synapse_client = NULL )

Pros

  • Simple for straightforward use cases.

  • No explicit object construction required.

Cons

  • Function signatures can become very long.

  • Constructor and method arguments are mixed together.

  • Harder to discover and understand through tab completion and documentation.

Trade-off Summary

Key trade-off: The main tension is between minimizing upfront object construction for simple calls and preserving a clean, scalable, and understandable API as entity complexity grows.

  • Option 1 appears to offer the best balance between clarity, composability, and API scalability.

  • Option 2 favors explicitness and convention.

  • Option 3 favors convenience, but risks bloated interfaces.

Current Recommendation

Proposed direction for discussion: Option 1, Instance-First with Pipe.

This option seems strongest because it keeps constructor concerns separate from method concerns, supports concise signatures, aligns with modern R workflows, and may allow us to consolidate entity-specific methods into more generic verbs.

Questions for Reviewers

  • Which option feels most natural from an R user's perspective?

  • Are there cases where pipe-based method calls would be awkward or unclear?

  • How important is alignment with classic S3-style conventions versus tidyverse-style workflows?

  • Would generic verbs such as synSnapshot() improve the API, or make it less explicit?

  • Are there alternative patterns we should consider before making a decision?