Domyślne argumenty funkcji w Rust

111

Czy można utworzyć funkcję z domyślnym argumentem?

fn add(a: int = 1, b: int = 2) { a + b }
Jeroen
źródło
4
# 6973 zawiera kilka obejść (przy użyciu struktury).
huon
Jak można to zakodować w 2020 roku?
puentesdiaz
@puentesdias Zaakceptowana odpowiedź jest nadal poprawną odpowiedzią. Nie ma sposobu, aby to zrobić w Rust i musisz albo napisać makro, albo użyć Optioni jawnie przekazać None.
Jeroen

Odpowiedzi:

58

Nie, obecnie nie ma. Myślę, że jest prawdopodobne, że w końcu zostanie wdrożony, ale obecnie nie ma aktywnej pracy w tej przestrzeni.

Typową techniką stosowaną tutaj jest użycie funkcji lub metod o różnych nazwach i podpisach.

Chris Morgan
źródło
2
@ ner0x652: ale pamiętaj, że takie podejście jest oficjalnie odradzane.
Chris Morgan
@ChrisMorgan Czy masz źródło, dla którego oficjalnie się to zniechęca?
Jeroen
1
@JeroenBollen Najlepsze, co mogę wymyślić w ciągu kilku minut, to reddit.com/r/rust/comments/556c0g/… , gdzie masz ludzi takich jak brson, który był wówczas liderem projektu Rust. IRC mógł mieć więcej, nie jestem pewien.
Chris Morgan
116

Ponieważ domyślne argumenty nie są obsługiwane, możesz uzyskać podobne zachowanie używając Option<T>

fn add(a: Option<i32>, b: Option<i32>) -> i32 {
    a.unwrap_or(1) + b.unwrap_or(2)
}

Osiąga to cel polegający na tym, że wartość domyślna i funkcja są kodowane tylko raz (zamiast w każdym wywołaniu), ale oczywiście jest o wiele więcej do wpisania. Wywołanie funkcji będzie wyglądać add(None, None), co może ci się spodobać lub nie, w zależności od twojej perspektywy.

Jeśli widzisz, że nic nie wpisuje się na liście argumentów, ponieważ koder potencjalnie zapomina o dokonaniu wyboru, to dużą zaletą jest tutaj jawność; wywołujący wyraźnie mówi, że chce korzystać z domyślnej wartości i otrzyma błąd kompilacji, jeśli nic nie poda. Pomyśl o tym jak o pisaniu add(DefaultValue, DefaultValue).

Możesz także użyć makra:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

macro_rules! add {
    ($a: expr) => {
        add($a, 2)
    };
    () => {
        add(1, 2)
    };
}
assert_eq!(add!(), 3);
assert_eq!(add!(4), 6);

Duża różnica między tymi dwoma rozwiązaniami polega na tym, że w przypadku argumentów -al „Opcja” zapisywanie jest całkowicie poprawne add(None, Some(4)), ale przy dopasowaniu wzorca makra nie można (jest to podobne do domyślnych reguł argumentów Pythona).

Możesz także użyć struktury „arguments” i From/ Intotraits:

pub struct FooArgs {
    a: f64,
    b: i32,
}

impl Default for FooArgs {
    fn default() -> Self {
        FooArgs { a: 1.0, b: 1 }
    }
}

impl From<()> for FooArgs {
    fn from(_: ()) -> Self {
        Self::default()
    }
}

impl From<f64> for FooArgs {
    fn from(a: f64) -> Self {
        Self {
            a: a,
            ..Self::default()
        }
    }
}

impl From<i32> for FooArgs {
    fn from(b: i32) -> Self {
        Self {
            b: b,
            ..Self::default()
        }
    }
}

impl From<(f64, i32)> for FooArgs {
    fn from((a, b): (f64, i32)) -> Self {
        Self { a: a, b: b }
    }
}

pub fn foo<A>(arg_like: A) -> f64
where
    A: Into<FooArgs>,
{
    let args = arg_like.into();
    args.a * (args.b as f64)
}

fn main() {
    println!("{}", foo(()));
    println!("{}", foo(5.0));
    println!("{}", foo(-3));
    println!("{}", foo((2.0, 6)));
}

Ten wybór to oczywiście o wiele więcej kodu, ale w przeciwieństwie do projektu makra wykorzystuje system typów, co oznacza, że ​​błędy kompilatora będą bardziej pomocne dla użytkownika biblioteki / API. Pozwala to również użytkownikom na tworzenie własnych Fromimplementacji, jeśli jest to dla nich pomocne.

ampron
źródło
3
ta odpowiedź byłaby lepsza jako kilka odpowiedzi, po jednej dla każdego podejścia. Chcę zagłosować za tylko jednym z nich
joel
60

Nie, Rust nie obsługuje domyślnych argumentów funkcji. Musisz zdefiniować różne metody o różnych nazwach. Nie ma też przeciążania funkcji, ponieważ Rust używa nazw funkcji do wyprowadzania typów (przeciążanie funkcji wymaga czegoś przeciwnego).

W przypadku inicjalizacji struktury możesz użyć składni aktualizacji struktury w następujący sposób:

use std::default::Default;

#[derive(Debug)]
pub struct Sample {
    a: u32,
    b: u32,
    c: u32,
}

impl Default for Sample {
    fn default() -> Self {
        Sample { a: 2, b: 4, c: 6}
    }
}

fn main() {
    let s = Sample { c: 23, ..Sample::default() };
    println!("{:?}", s);
}

[na życzenie przesłałem tę odpowiedź z drugiego pytania]

eulerdisk
źródło
5
Jest to bardzo użyteczny wzorzec dla domyślnych argumentów. Powinien być wyżej
Ben
10

Rust nie obsługuje domyślnych argumentów funkcji i nie wierzę, że zostanie zaimplementowany w przyszłości. Napisałem więc proc_macro duang, aby zaimplementować go w postaci makra.

Na przykład:

duang! ( fn add(a: i32 = 1, b: i32 = 2) -> i32 { a + b } );
fn main() {
    assert_eq!(add!(b=3, a=4), 7);
    assert_eq!(add!(6), 8);
    assert_eq!(add(4,5), 9);
}
zhengmian hu
źródło
7

Jeśli używasz Rust 1.12 lub nowszego, możesz przynajmniej ułatwić używanie argumentów funkcji z Optioni into():

fn add<T: Into<Option<u32>>>(a: u32, b: T) -> u32 {
    if let Some(b) = b.into() {
        a + b
    } else {
        a
    }
}

fn main() {
    assert_eq!(add(3, 4), 7);
    assert_eq!(add(8, None), 8);
}
kałamarnice
źródło
7
Chociaż technicznie poprawne, społeczność Rusta jest wyraźnie podzielona co do tego, czy jest to „dobry” pomysł. Osobiście zaliczam się do obozu „niedobrego”.
Shepmaster
1
@Shepmaster może zwiększyć rozmiar kodu i nie jest super czytelny. Czy to obiekcje wobec używania tego wzorca? Do tej pory stwierdziłem, że kompromisy są opłacalne w obsłudze ergonomicznych interfejsów API, ale uważam, że może mi brakować innych problemów.
kałamarnice
2

Innym sposobem mogłoby być zadeklarowanie wyliczenia z opcjonalnymi parametrami jako wariantami, które można sparametryzować tak, aby przyjmowały odpowiedni typ dla każdej opcji. Funkcję można zaimplementować w celu pobrania wycinka o zmiennej długości wariantów wyliczeniowych. Mogą być w dowolnej kolejności i długości. Wartości domyślne są implementowane w funkcji jako przypisania początkowe.

enum FooOptions<'a> {
    Height(f64),
    Weight(f64),
    Name(&'a str),
}
use FooOptions::*;

fn foo(args: &[FooOptions]) {
    let mut height   = 1.8;
    let mut weight   = 77.11;
    let mut name     = "unspecified".to_string();

    for opt in args {
        match opt {
            Height(h) => height = *h,
            Weight(w) => weight = *w,
            Name(n)   => name   =  n.to_string(),
        }
    }
    println!("  name: {}\nweight: {} kg\nheight: {} m", 
             name, weight, height);
}

fn main() { 

            foo( &[ Weight(90.0), Name("Bob") ] );

}

wynik:

  name: Bob
weight: 90 kg
height: 1.8 m
Todd
źródło