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

Enum Support #23

Closed
Deaths-Door opened this issue Jul 30, 2024 · 5 comments
Closed

Enum Support #23

Deaths-Door opened this issue Jul 30, 2024 · 5 comments
Labels
design needed The feature requires more design effort feature request A new feature is requested

Comments

@Deaths-Door
Copy link

I'm interested in using your tool but it currently lacks support for Rust enums. I propose extending the macro to handle enums as well.

For unit enums, the macro could either ignore them or generate a dummy function. For example

impl $enum {
   fn $variant () {
      $enum :: $variant
   }
}

For unnamed enums, I'm unsure whether generating no code or implementing a default 'from' function for each variant is the best approach. For example

impl $enum {
    fn $variant (value : impl Into<$inner_field_type>) {
       $enum :: $variant ( value.into())
    }
}

For named enums, we create a builder struct, generate builder methods for each field, and modify the finish_fn to return the enum variant instead of the builder struct. Additionally, a start_fn would be generated to create a new builder instance from the enum. So

impl $enum {
    fn $variant ()  -> #builder {
       #builder :: #inner_start_fn
    }
}
@Veetaha Veetaha added feature request A new feature is requested design needed The feature requires more design effort labels Jul 30, 2024
@Veetaha
Copy link
Contributor

Veetaha commented Jul 30, 2024

I haven't seen a need for enum builders on my experience. I usually write enums with tuple-like variants that have struct types embedded in them:

enum Foo { 
    A(AStruct)
    B(BStruct)
}

struct AStruct { field: String, field2: u32 }
struct BStruct { field bool }

It makes it possible to extract that struct from the enum and process it outside of a match. So I usually don't write enums like this because they don't usually scale that good:

enum Foo {
    A { field: String, field2: u32 },
    B { field: bool }
}

I'd like to know more about your use case and how builder for enum would solve it.

From my side, I can tell that I have another good proc macro currently in a private repo that I may potentially open-source (maybe as part of bon). I use it to generate From<Variant> implementations for enums plus various methods to downcast the enum into its variants.

Here is the snippet of its documentation:


Derives various From/TryFrom conversions for the enum to its variants.
Also adds the following methods for each variant:

  • is_* -> bool
  • as_* -> Option<&T>
  • as_mut_* -> Option<&mut T>
  • into_* -> Option<T>
  • try_as_* -> Result<&T>
  • try_as_mut_* -> Result<&mut T>
  • try_into_* -> Result<T>
  • unwrap_as_* -> &T
  • unwrap_as_mut_* -> &mut T
  • unwrap_* -> T
  • unwrap::<T> -> T
  • unwrap_ref::<T> -> &T
  • unwrap_mut::<T> -> &mut T

Having such a macro allows you to avoid building enums directly. Instead you'd define enums as pure sum types, which you can create out of structs that derive builders.

For example:

use bon::builder;

#[derive(Sum)]
enum Foo {
    A(A),
    B(B),
} 

#[builder]
struct A {}

#[builder]
struct B {}

// Create the enum:

let enum_value: Foo = A::builder().build().into();
let a: A = enum_value.unwrap_a();

let enum_value: Foo = B::builder().build().into();
let b: B = enum_value.unwrap_b();

@Veetaha
Copy link
Contributor

Veetaha commented Jul 30, 2024

I'd suggest discussing alternative simpler approaches first like the one above with generating From implementations for enums. However, I'll leave some comments about the design currently proposed in the issue.

Drawing some inspiration from this PR into typed-builder is a good place to start: idanarye/rust-typed-builder#129.

For example, there were some good points raised about naming of methods. Enums usually have methods named after variants to implement getters like Result::ok and Result::err. I'd probably think of having Enum::builder() method that returns a builder first which then has a method to start the construction of each variant.

@Deaths-Door
Copy link
Author

Deaths-Door commented Jul 30, 2024

While I typically prefer using structs for clarity when dealing with more than three fields, I've found that for simpler cases with one to three fields, directly using enum variants is often sufficient. My current project involves building an expression tree with Term, Custom, and Binary { op, left, right } variants. Initially, using new_* functions worked well, but as the logic became more complex, with conditional logic and other things, I encountered limitations for which I think a Builder-like API could help.

Having such a macro allows you to avoid building enums directly. Instead you'd define enums as pure sum types, which you can create out of structs that derive builders.

For instance, relying on methods like unwrap_a() can introduce potential error points , which a builder for each variant would solve.

I'd probably think of having Enum::builder() method that returns a builder first which then has a method to start the construction of each variant.

I'd probably lean towards having an Enum::variant_builder() method to directly initiate the construction of a specific variant. And in my opinion it provides more clarity about the intended variant that one wants to build

PS - Didn't see your comment before opening the PR

@Veetaha
Copy link
Contributor

Veetaha commented Jul 30, 2024

For instance, relying on methods like unwrap_a() can introduce potential error points , which a builder for each variant would solve.

That unwrap_a() method is a bit different story. It's a method on an enum that downcasts it to the variant A. It panics if enum's current variant is something different. It's not actually related to the builder itself. I guess let's probably ignore it for the purposes of this discussion.


Another comment about your use case. Did you consider defining builders for your enums via methods?

For example:

use bon::bon;

enum Expression {
    Number(u32),
    Binary {
        op: String,
        left: Box<Self>,
        right: Box<Self>,
    }
}

#[bon]
impl Expression {
     #[builder(finish_fn = build)]
     fn binary(
         op: String,
         
         #[builder(into)]
         left: Box<Self>,
         
         #[builder(into)]
         right: Box<Self>,
     ) -> Self {
         Self::Binary { op, left, right }
     }
}

Expression::binary()
    .op("==")
    .left(Expression::Number(1))
    .right(Expression::Number(3))
    .build();

It is a bit more code to write, but it generates the builder that you'd like. This will let you have a builder for your enum without the need to go the long-long route of designing and implementing builders for enums in bon. This has been my original vision of how people would use bon to create builders for their enums without having support for #[builder] on enums directly (which is a complex feature).

Basically defining #[bon::builder] on a method solves any complex use case. It also makes the code more flexible and simple as it allows you to do anything you want inside of the method to build your expression tree.

@Deaths-Door
Copy link
Author

You're right, I didn't seem think of that. It actually solves the problem. I'll close this then

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design needed The feature requires more design effort feature request A new feature is requested
Projects
None yet
Development

No branches or pull requests

2 participants