-
-
Notifications
You must be signed in to change notification settings - Fork 119
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
Make defining models more convenient #72
Comments
If you use proc_macros you can make them a single function with arguments and expand that into whatever you need to meet the C interface required. #[fj::model]
pub fn model(outer: f64, inner: f64, height: f64) -> fj::Shape { /* ... */ } A bit more effort to define the proc_macro, but allows you to change the underlying implementation in the future if required. And keeps a nice simple interface for users to use with minimal boiler plate. |
Thanks for the suggestion, @mdaffin! Yes, something like that should be possible. The only thing I'm not sure about, is if proc macros can add attributes, so maybe something like I actually like your suggested syntax better than my own! It's just simpler and cleaner. |
Proc macros can do anything. They just take in a token stream and output a possibly different token stream. For instance, the test_case proc macro adds the |
Ah, good to know, thanks! I've used proc macros before, but never to add attributes. I had this worry in the back of my mind about order of execution in the compiler, because proc macros often are attribute, so can they add something that's already been read at point in compilation. But good to know my worries were unfounded! |
Relevant thoughts: #220 (comment) |
I played around with procedural macros today and roughly implemented this use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
#[proc_macro_attribute]
pub fn model(default_values: TokenStream, input: TokenStream) -> TokenStream {
let vals: Vec<String> = default_values
.into_iter()
.filter_map(|tree| {
if let proc_macro::TokenTree::Literal(lit) = tree {
Some(lit.to_string())
} else {
None
}
})
.collect();
let item = parse_macro_input!(input as syn::ItemFn);
let inputs = item.sig.inputs;
let mut names = Vec::new();
let mut types = Vec::new();
for f in inputs.iter() {
if let syn::FnArg::Typed(meep) = f {
if let syn::Pat::Ident(ident) = *meep.clone().pat {
names.push(ident.ident);
}
if let syn::Type::Path(path) = *meep.clone().ty {
types.push(path.path.get_ident().unwrap().clone());
}
}
}
let block = item.block;
quote! {
#[no_mangle]
pub extern "C" fn model(args: &HashMap<String, String>) -> fj::Shape {
#(let #names: #types = args.get(stringify!(#names)).map(|arg| arg.parse().unwrap()).unwrap_or(#vals.parse::<#types>().unwrap());)*
#block
}
}.into()
} This allows one to create a model by specifying: #[fj_proc::model(5, 1.0, 2.0, 1.0)]
pub fn model(num_points: u64, r1: f64, r2: f64, h: f64) -> fj::Shape {
....
} which will expand to: #[no_mangle]
pub extern "C" fn model(args: &HashMap<String, String>) -> fj::Shape {
let num_points: u64 = args
.get("num_points")
.map(|arg| arg.parse().unwrap())
.unwrap_or("5".parse::<u64>().unwrap());
let r1: f64 = args
.get("r1")
.map(|arg| arg.parse().unwrap())
.unwrap_or("1.0".parse::<f64>().unwrap());
let r2: f64 = args
.get("r2")
.map(|arg| arg.parse().unwrap())
.unwrap_or("2.0".parse::<f64>().unwrap());
let h: f64 = args.get("h").map(|arg| arg.parse().unwrap()).unwrap_or("1.0".parse::<f64>().unwrap());
...
} Do you think this would be a good way to deal with the ergonomics? @hannobraun |
Thanks for looking into this, @gabsi26! I think what you show here is definitely a step in the right direction. The one aspect I don't like, is that you can't associate parameters with their default values at a glance. You basically have to count. I'm not sure what the best solution is, but maybe something like this: #[fj::model]
pub fn model(params: Params) -> fj::Shape {
// ...
}
#[derive(fj::Params)]
pub struct Params {
#[default(5)]
pub num_points: u64,
#[default(1.0)]
pub r1: f64,
#[default(2.0)]
pub r2: f64,
#[default(1.0)]
pub h: f64,
} That would be verbose, but explicit. Or, we can do something similar to what RTIC does, and generate the parameters struct: #[fj::model(num_points: u64 = 5, r1: f64 = 1.0, r2: f64 = 2.0, h: f64 = 1.0)]
pub fn model(p: model::Params) -> fj::Shape {
// Access parameters through the generated struct
for i in 0..p.num_points {
// ...
}
} As I said, not sure, I'm just throwing out idea. And as I also said initially, what you posted initially is definitely a step in the right direction. I'd merge that, if you submitted it as a pull request (pending proper review; haven't looked at the proc macro code in detail). One thing we have to be careful about though, is the impact on model compile times. Short compile times are important to enable productive modeling. By the way, you can get syntax highlighting for the code in your comments using this syntax:
|
I am not a big fan of wrapping things into a struct just to pass them to a function. Another couple of options: #[fj::model(num_points = 5, r1 = 1.0, r2 = 2.0, h = 1.0)]
pub fn model(num_points: u64, r1: f64, r2: f64, h: f64) -> fj::Shape { Has some duplication, but I still think overall better - does not hide the fields behind a generated struct. #[fj::model]
pub fn model(
#[default(5)] num_points: u64,
#[default(1.0)] r1: f64,
#[default(2.0)] r2: f64,
#[default(1.0)] h: f64,
) -> fj::Shape { If a bit more verbose than the above but also less disjointed. Or you could just reply on Default trait and Option: #[fj::model]
pub fn model(
num_points: Option<u64>,
r1: Option<f64>,
r2: Option<f64>,
h: Option<f64>,
) -> fj::Shape {
let num_points = num_points.unwrap_or(5);
let r1 = r1.unwrap_or(1.0);
let r2 = r2.unwrap_or(2.0);
let h = h.unwrap_or(1.0); Though this is just as verbose as a extra struct. Some things you might also want to consider - do you want to provide hints to the UI for things like min/max values as well? |
Thank you for the additional suggestions, @mdaffin!
I like this one best, although I don't like it better than my "struct with derive" suggestion. I do like it better than my "generated struct" suggestion. I don't like the other ones, due to the drawbacks you mention.
Yes! Doesn't need be part of the first iteration, but eventually, there should be enough information so models don't have to worry much about parameter-related error handling. And enough information to generate a nice HTML form, for example (or some custom bit of UI). Your question triggered a new idea: How about, we encode the required metadata as part of the parameter type, not the model function? Something like this: #[fj::model]
pub fn model(num_points: NumPoints, r1: InnerRadius, r2: OuterRadius, h: Height) -> fj::Shape {
// ...
}
#[derive(fj::Param(default = 5, min = 3, max = u64::MAX))]
pub struct NumPoints(u64);
#[derive(fj::Param(default = 1.0, min = 0.1))]
pub struct InnerRadius(f64);
// etc. There would be implementations of #[fj::model]
pub fn model(num_points: u64, r1: f64, r2: f64, h: f64) -> fj::Shape {
// ...
} |
I'm going to close this issue now. Its purpose is fulfilled, defining models is more convenient. I have opened two more specific follow-up issues: |
The
extern "C"
stuff required for the dynamic library isn't great. And the way parameters are passed to the model has caused further pain. Ideally, creating a model should require little or no ceremony to satisfy the plugin system. Maybe something like this would be good:This is just a sketch, not to be taken too seriously. Maybe it's possible, or maybe a
#[no_mangle]
or something will still be required.The text was updated successfully, but these errors were encountered: