Idea: Support for global substitutions, and for generating items once / starting a new `akin` context
d4h0 opened this issue · 5 comments
Hi @LyonSyonII,
First of all, thanks for creating akin
. I like the approach quit a lot.
I'm wondering if global substitutions, and generating items only once can be added to akin
.
An example describes the use case best:
Imagine we would like to create an extension trait for serde_json::Value that adds methods like into_xy
methods that return owned values.
I'd create and implement a trait like this:
trait ValueExt {
fn into_null(self) -> Result<(), Self>;
fn into_bool(self) -> Result<bool, Self>;
fn into_array(self) -> Result<Vec<Value>, Self>;
// etc...
}
impl ValueExt for Value {
fn into_null(self) -> Result<(), Self> { todo!()}
fn into_bool(self) -> Result<bool, Self> { todo!()}
fn into_array(self) -> Result<Vec<Value>, Self> { todo!()}
// etc...
}
Instead of writing all of this by hand, I'd like to use akin
to generate the trait and the trait implementation:
akin::akin! {
let &ident = [ null, bool, array, /* ... */ ];
let &Type = [
{ () },
{ bool },
{ Vec<Value> },
// ...
];
trait JsonValueExt {
fn into_~*ident(self) -> Result<*Type, Self>;
}
impl ValueExt for serde_json::Value {
fn into_~*ident(self) -> Result<*Type, Self> { todo!() }
}
}
But of course this doesn't work (JsonValueExt
is defined several times).
I'm wondering if this use case somehow can be supported.
For example via something like this:
akin::akin! {
// ...
#[akin(context)]
trait JsonValueExt {
// A new `akin` context
fn into_~*ident(self) -> Result<*Type, Self>;
}
// ...
}
#[akin(context)]
would tell akin
to generate the decorated item only once, and substitute within the item (similar to how $($ident)+
works for macro_rules
macros). Something like akin_context! { /* new context */ }
probably also would be useful, for cases where attributes can't be used.
My first idea for a name of the new attribute was inner
, but I think that wouldn't be ideal, because it should be possible to substitute things within the trait definition itself (e.g. for generic traits). Maybe there is a better name than context
, however.
Similarly, the duplicate
crate has a nice feature to define global substitutions, which often come in handy.
I'm wondering if this can be supported via something like:
akin::akin! {
// ...
const &foo = { println!("Hello, world!") };
*foo
// ...
}
...which would expand to just one println!("Hello, world!")
.
Using const
here would be perfect semantically, I believe.
These features would make akin
useful in even more cases.
Currently, I have created an additional macro_rules
macro to generate the trait definition (duplicating the &Type
and &ident
definitions), which works but is not really ideal.
Would it be possible to add these features?
Currently, I haven't learned how to use procedural macros, so I currently can't add these features myself. But if you don't want to add this yourself, but would accept a PR, I could do this when I learn procedural macros at some point (however, that would most likely be at least several months into the future, if not more).
Thanks again for creating akin
!
Thank you for making this issue, I'm happy someone uses akin
for real projects!
If I understand correctly, the main problem is that the JsonValueExt
trait is being defined multiple times.
I believe it can be solved without introducing new features, by just creating a new variable with the code you want to duplicate, like with the match
example on the README.
So, the code would end up looking like this:
let &ident = [ null, bool, array, /* ... */ ];
let &Type = [
{ () },
{ bool },
{ Vec<Value> },
// ...
];
let &trait_fn = {
fn into_~*ident(self) -> Result<*Type, Self>;
};
let &impl_fn = {
fn into_~*ident(self) -> Result<*Type, Self> {
todo!()
}
};
trait JsonValueExt {
*trait_fn
}
impl JsonValueExt for serde_json::Value {
*impl_fn
}
This way, both trait
and impl
are kept intact and each fn
is copied the correct number of times.
If this is too verbose or repetitive for you (understandable being the purpose of the crate to avoid these situations), I can look into adding the akin_context!
macro, as it's more in line with how akin
works currently and it would work great as syntactic sugar for the solution I've shown you (basically treating each akin_context!
as a new variable).
Regarding the global substitutions, I don't quite understand their purpose, could you make a more elaborate example?
@LyonSyonII: Thank you! Somehow I missed / didn't realize that it works this way.
I was able to get the simple serde_json::Value
example fully working (or rather, compiling). With my actual code I somehow get a "duplicate methods" error, but that is probably a bug in my code.
Regarding the global substitutions, I don't quite understand their purpose, could you make a more elaborate example?
I'm not 100% sure, but this might already work with the same approach.
Here is an example from the documentation of duplicate
that demonstrates the use case:
#[duplicate_item(
typ1 [Some<Complex<()>, Type<WeDont<Want, To, Repeat>>>];
typ2 [Some<Other, Complex<Type<(To, Repeat)>>>];
)]
fn some_func(arg1: typ1, arg2: typ2) -> (typ1, typ2){
...
}
If this is already possible, then this issue can be closed, from my side.
That being said, I think the akin(context)
attribute would make the code easier to understand, because everything is in one place together, in (more or less) regular Rust. For short macros this isn't a problem, but longer macros would benefit from this, I think.
In case anybody who stumbles upon this issue is interested in it, here is a full implementation of the JsonValueExt
trait:
pub struct JsonValueConversionError(serde_json::Value);
akin::akin! {
let &ident = [ bool, number, string, array, object ];
let &Variant = [ Bool, Number, String, Array, Object ];
let &Type = [
{ bool },
{ serde_json::value::Number },
{ String },
{ Vec<serde_json::Value> },
{ serde_json::value::Map<String, serde_json::Value> },
];
let &trait_fn = {
fn into_~*ident(self) -> Result<*Type, JsonValueConversionError>;
};
let &impl_fn = {
fn into_~*ident(self) -> Result<*Type, JsonValueConversionError> {
if let serde_json::Value::*Variant(v) = self {
return Ok(v)
}
Err(JsonValueConversionError(self))
}
};
trait JsonValueExt {
fn into_null(self) -> Result<(), JsonValueConversionError>;
*trait_fn
}
impl JsonValueExt for serde_json::Value {
fn into_null(self) -> Result<(), JsonValueConversionError> {
if let serde_json::Value::Null = self {
return Ok(())
}
Err(JsonValueConversionError(self))
}
*impl_fn
}
}
With my actual code I somehow get a "duplicate methods" error, but that is probably a bug in my code.
I was able to figure out what the problem was.
In case anyone gets the same error, the following code demonstrates the problem:
struct Foo;
akin::akin! {
let &into = [u8, u16];
let &stuff = [a, b, c];
let &foo_impl_fn = {
fn other_~*stuff(self) {}
};
impl Foo {
*foo_impl_fn
}
impl Into<*into> for Foo {
fn into(self) -> *into { *into::MIN }
}
}
This leads to 'duplicate definitions with name other_a
' errors.
The solution is, to put the Into
trait implementation into a code variable like this:
struct Foo;
akin::akin! {
let &into = [u8, u16];
let &stuff = [a, b, c];
let &foo_impl_fn = {
fn other_~*stuff(self) {}
};
// Assign `Into` trait implementation to code variable:
let &foo_into_impl = {
impl Into<*into> for Foo {
fn into(self) -> *into { *into::MIN }
}
};
// Add trait implementation:
*foo_into_impl
impl Foo {
*foo_impl_fn
}
}
So basically "code variables" are like contexts, and it's probably a good idea to put every independent part into such a variable.
I can look into adding the akin_context! macro, as it's more in line with how akin works currently and it would work great as syntactic sugar for the solution I've shown you (basically treating each akin_context! as a new variable).
Damn, I missed this part...
@LyonSyonII: Yes, I think akin_context!
would be a nice addition.
It probably would also make akin
s concepts a bit easier to understand (with corresponding documentation).
I think my issue was, that I thought that every top level item (type definitions, implementations, etc.) gets treated separately. Maybe it would make sense to add an example with several top level items?
Thanks for posting the correction!
I'll add it (simplified) as an example for the misunderstanding you had, to show the idea that akin
works on any code, it has no concept of function declaration, etc.
akin_context!
will be added on a future version, I'll keep this issue open as a place to discuss how it'll work.
Sounds good 👍