LyonSyonII/akin

Idea: Support for global substitutions, and for generating items once / starting a new `akin` context

d4h0 opened this issue · 5 comments

d4h0 commented

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?

d4h0 commented

@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
    }
}
d4h0 commented

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 akins 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.

d4h0 commented

Sounds good 👍