├── .gitignore ├── Cargo.toml ├── LICENSE ├── readme.md ├── src └── lib.rs └── tests ├── smoke.rs └── with_map.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target/** 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cutlass" 3 | version = "0.1.0" 4 | authors = ["Akshay "] 5 | edition = "2018" 6 | description = "Macro based library to take the boilerplate out of configuration handling" 7 | readme = "readme.md" 8 | repository = "https://github.com/nerdypepper/cutlass" 9 | license = "MIT" 10 | keywords = ["curry", "currying", "macro", "functional", "haskell"] 11 | 12 | [dependencies] 13 | proc-macro2 = "1.0.9" 14 | quote = "1.0" 15 | 16 | [dependencies.syn] 17 | version = "1.0" 18 | features = ["full"] 19 | 20 | [lib] 21 | proc-macro = true 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Akshay Oppiliappan (nerdy@peppe.rs) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cutlass 2 | 3 | > experimental auto-currying for rust functions 4 | 5 | ### example 6 | 7 | this currently works only on nightly with the 8 | `type_alias_impl_trait` feature enabled. 9 | 10 | ```rust 11 | #![feature(type_alias_impl_trait)] 12 | 13 | #[cutlass::curry] 14 | fn add(x: u32, y: u32, z: u32) -> u32 { 15 | return x + y + z; 16 | } 17 | 18 | fn main() { 19 | let plus_3 = add(1)(2); 20 | let v: Vec = (1..=3).map(plus_3).collect(); 21 | assert_eq!(v, vec![4, 5, 6]); 22 | } 23 | ``` 24 | 25 | ### how it works 26 | 27 | the `#[curry]` proc-macro expands the above `add` function to 28 | something like this (roughly): 29 | 30 | ```rust 31 | type T0 = u32; 32 | type T1 = impl Fn(u32) -> T0; 33 | type T2 = impl Fn(u32) -> T1; 34 | fn add(x: u32) -> T2 { 35 | return (move |y| move |z| x + y + z); 36 | } 37 | ``` 38 | 39 | ### gotchas 40 | 41 | - doesn't yet work for method functions (signatures with 42 | `self`) 43 | - the function has to have a return value 44 | - works only on nightly with `type_alias_impl_trait` 45 | enabled 46 | 47 | ### testing 48 | 49 | test this crate with `cargo +nightly test`. 50 | 51 | to view macro expansions, install `cargo-expand` and run `cargo expand 52 | --test `. expand `tests/smoke.rs` for example: 53 | 54 | ``` 55 | cargo expand --test smoke 56 | ``` 57 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | use quote::{format_ident, quote}; 5 | use syn::punctuated::Punctuated; 6 | use syn::{parse_macro_input, Block, FnArg, ItemFn, Pat, ReturnType, Type}; 7 | 8 | #[proc_macro_attribute] 9 | pub fn curry(_attr: TokenStream, item: TokenStream) -> TokenStream { 10 | let parsed = parse_macro_input!(item as ItemFn); 11 | generate_curry(parsed).into() 12 | } 13 | 14 | fn extract_type(a: FnArg) -> Box { 15 | match a { 16 | FnArg::Typed(p) => p.ty, 17 | _ => panic!("Not supported on types with `self`!"), 18 | } 19 | } 20 | 21 | fn extract_arg_pat(a: FnArg) -> Box { 22 | match a { 23 | FnArg::Typed(p) => p.pat, 24 | _ => panic!("Not supported on types with `self`!"), 25 | } 26 | } 27 | 28 | fn extract_return_type(a: ReturnType) -> Box { 29 | match a { 30 | ReturnType::Type(_, p) => p, 31 | _ => panic!("Not supported on functions without return types!"), 32 | } 33 | } 34 | 35 | fn extract_arg_types(fn_args: Punctuated) -> Vec> { 36 | return fn_args.into_iter().map(extract_type).collect::>(); 37 | } 38 | 39 | fn extract_arg_idents(fn_args: Punctuated) -> Vec> { 40 | return fn_args.into_iter().map(extract_arg_pat).collect::>(); 41 | } 42 | 43 | fn generate_type_aliases( 44 | fn_arg_types: &[Box], 45 | fn_return_type: Box, 46 | fn_name: &syn::Ident, 47 | ) -> Vec { 48 | let type_t0 = format_ident!("_T0{}", fn_name); 49 | let mut type_aliases = vec![quote! { type #type_t0 = #fn_return_type }]; 50 | for (i, t) in (1..).zip(fn_arg_types.into_iter().rev()) { 51 | let p = format_ident!("_{}{}", format!("T{}", i - 1), fn_name); 52 | let n = format_ident!("_{}{}", format!("T{}", i), fn_name); 53 | type_aliases.push(quote! { 54 | type #n = impl Fn(#t) -> #p 55 | }) 56 | } 57 | return type_aliases; 58 | } 59 | 60 | fn generate_body(fn_args: &[Box], body: Box) -> proc_macro2::TokenStream { 61 | quote! { 62 | return #( move |#fn_args| )* #body 63 | } 64 | } 65 | 66 | fn generate_curry(parsed: ItemFn) -> proc_macro2::TokenStream { 67 | let fn_body = parsed.block; 68 | let sig = parsed.sig; 69 | let vis = parsed.vis; 70 | let fn_name = sig.ident; 71 | let fn_args = sig.inputs; 72 | let fn_return_type = sig.output; 73 | 74 | let arg_idents = extract_arg_idents(fn_args.clone()); 75 | let first_ident = &arg_idents.first().unwrap(); 76 | let curried_body = generate_body(&arg_idents[1..], fn_body.clone()); 77 | 78 | let arg_types = extract_arg_types(fn_args.clone()); 79 | let first_type = &arg_types.first().unwrap(); 80 | let type_aliases = generate_type_aliases( 81 | &arg_types[1..], 82 | extract_return_type(fn_return_type), 83 | &fn_name, 84 | ); 85 | 86 | let return_type = format_ident!("_{}{}", format!("T{}", type_aliases.len() - 1), &fn_name); 87 | 88 | let gen = quote! { 89 | #(#type_aliases);* ; 90 | #vis fn #fn_name (#first_ident: #first_type) -> #return_type { 91 | #curried_body ; 92 | } 93 | }; 94 | 95 | println!("{}", gen); 96 | return gen; 97 | } 98 | -------------------------------------------------------------------------------- /tests/smoke.rs: -------------------------------------------------------------------------------- 1 | #![feature(type_alias_impl_trait)] 2 | 3 | #[cutlass::curry] 4 | fn add(x: u32, y: u32, z: u32) -> u32 { 5 | return x + y + z; 6 | } 7 | 8 | #[test] 9 | fn add_works() { 10 | let plus_3 = add(1)(2); 11 | let v: Vec = (1..=3).map(plus_3).collect(); 12 | assert_eq!(v, vec![4, 5, 6]); 13 | } 14 | -------------------------------------------------------------------------------- /tests/with_map.rs: -------------------------------------------------------------------------------- 1 | #![feature(type_alias_impl_trait)] 2 | 3 | pub mod helper_functions { 4 | use cutlass::curry; 5 | 6 | #[curry] 7 | pub fn product(x: u32, y: u32) -> u32 { 8 | return x * y; 9 | } 10 | #[curry] 11 | pub fn add(x: u32, y: u32) -> u32 { 12 | x + y 13 | } 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use super::helper_functions::{add, product}; 19 | #[test] 20 | fn test_product() { 21 | assert_eq!( 22 | (1..=3).map(product(5)).collect::>(), 23 | vec![5, 10, 15] 24 | ); 25 | } 26 | 27 | #[test] 28 | fn test_add() { 29 | let increment = add(1); 30 | let v: Vec = (1..=3).map(increment).collect(); 31 | assert_eq!(v, vec![2, 3, 4]); 32 | } 33 | } 34 | --------------------------------------------------------------------------------