Общий параметр по умолчанию

У меня есть структура, которую мы можем построить с помощью шаблона построителя, потому что есть несколько полей Optional.

Если я использую функции построителя для указания этих необязательных полей, мне не нужно указывать общие параметры.

Но если я не вызываю эти функции, мне нужно указать общие параметры.

Вот пример:

use Structs::*;

struct Struct<T, F: Fn(T)> {
    func: Option<F>,
    value: T,
}

enum Structs<T, F: Fn(T)> {
    Struct1(T),
    Struct2(T, F),
}

impl<T, F: Fn(T)> Struct<T, F> {
    fn new(value: T) -> Struct<T, F> {
        Struct {
            func: None,
            value: value,
        }
    }

    fn build(self) -> Structs<T, F> {
        if let Some(func) = self.func {
            Struct2(self.value, func)
        }
        else {
            Struct1(self.value)
        }
    }

    fn func(mut self, func: F) -> Struct<T, F> {
        self.func = Some(func);
        self
    }
}

fn main() {
    let _strct = Struct::new(42)
        .func(|n| { println!("{}", n); })
        .build();

    //let _strct = Struct::new(42).build(); // Does not compile.
    let _strct = Struct::<_, &Fn(_)>::new(42).build();
}

Я хотел бы опустить аннотацию типа, когда необязательные поля не установлены, например:

let _strct = Struct::new(42).build();

Следует указать, что тип F зависит от T.

Я попытался указать параметр типа по умолчанию как таковой:

impl<T, F: Fn(T) = Box<Fn(T)>> Struct<T, F> {

но это не решает проблему.

Итак, как мне избежать необходимости указывать параметры типа в вызове Struct::new()?

Если этого невозможно избежать, есть ли какие-либо альтернативы шаблону построителя, которые позволили бы мне опустить аннотацию типа?


person antoyo    schedule 18.05.2016    source источник
comment
Если вы не укажете конкретный тип для T или F, то сколько места компилятор Rust должен выделить для хранения Struct?   -  person Shepmaster    schedule 19.05.2016
comment
@Shepmaster, теоретически, если компилятор может определить, что F никогда не используется, он может просто записать это поле как размер 0. Проблема, конечно, в том, что такого рода вывод всей программы действительно сложен.   -  person LinearZoetrope    schedule 19.05.2016
comment
@J или интересный момент, но, как вы упомянули, Rust не делает никаких выводов за пределами одной границы функции.   -  person Shepmaster    schedule 19.05.2016
comment
@Shepmaster, можете ли вы уточнить или дать некоторые ссылки на утверждение, что Rust не делает никаких выводов за пределами одной границы функции?   -  person malbarbo    schedule 19.05.2016


Ответы (2)


Следуя умному решению Фрэнсиса Гагне, вот аналогичная идея, которая может работать на стабильном Rust:

struct Struct<T, F: Fn(T)> {
    func: Option<F>,
    value: T,
}

enum Structs<T, F: Fn(T)> {
    Struct1(T),
    Struct2(T, F),
}

impl<T> Struct<T, fn(T)> {
    fn new(value: T) -> Struct<T, fn(T)> {
        Struct {
            func: None,
            value: value,
        }
    }
}

impl<T, F: Fn(T)> Struct<T, F> {
    fn func<F2: Fn(T)>(self, func: F2) -> Struct<T, F2> {
        Struct {
            func: Some(func),
            value: self.value,
        }
    }

    fn build(self) -> Structs<T, F> {
        use Structs::*;

        if let Some(func) = self.func {
            Struct2(self.value, func)
        } else {
            Struct1(self.value)
        }
    }
}

fn main() {
    let _strct = Struct::new(42)
        .func(|n| {
            println!("{}", n);
        })
        .build();

    let _strct = Struct::new(42).build();
}

Вместо ясного типа Void мы просто говорим, что возвращаем структуру, которая будет параметризована для указателя функции. Вы также можете указать объект ссылочного признака:

impl<T> Struct<T, &'static Fn(T)> {
    fn new(value: T) -> Struct<T, &'static Fn(T)> {

Отвечая на свой вопрос из комментария:

Если вы не укажете конкретный тип для T или F, то сколько места компилятор Rust должен выделить для хранения Struct?

Размер fn() составляет 8 байтов на 64-битной машине, что дает в общей сложности 16 байтов для всей структуры:

std::mem::size_of::<fn()>();
std::mem::size_of_val(&strct);

Однако, когда вы даете ему конкретный обратный вызов, структура занимает всего 8 байтов! Это потому, что тип будет мономорфизирован для обратного вызова, который не требует состояния и может быть в основном встроен.

Решение Фрэнсиса Ганье требует только 8 байтов в каждом случае, так как тип Void имеет нулевой размер!

person Shepmaster    schedule 19.05.2016
comment
Теоретически Struct<T, Void> можно было бы еще больше оптимизировать: поскольку вариант Some Option<Void> не может быть создан, остается только вариант None. Перечисление с одним вариантом не требует дискриминанта (хотя сейчас кажется, что дискриминант для единичного перечисления все еще существует), а поскольку None не содержит никаких данных, размер Option<Void> может быть равен нулю. Таким образом, размер Struct<i32, Void> будет 4, а не 8. - person Francis Gagné; 20.05.2016

Есть способ решить эту проблему, изменив тип строителя по мере его развития. Поскольку Struct::func становится владельцем построителя и возвращает новый построитель, мы можем изменить тип результата.

Во-первых, нам нужно указать начальный тип для F. Мы могли бы просто выбрать любую существующую реализацию для Fn(T), но мы можем сделать лучше. Я предлагаю использовать пустой / void / unhabited / bottom тип, чтобы было ясно, что когда F - это этот тип, тогда Option - это None (вы не можете построить Some(x), потому что нет действительного x для пустого типа). Одним из недостатков этого подхода является то, что реализация Fn, FnMut и FnOnce для типов (кроме замыканий) нестабильна и требует ночного компилятора.

#![feature(fn_traits)]
#![feature(unboxed_closures)]

enum Void {}

impl<T> FnOnce<T> for Void {
    type Output = ();

    extern "rust-call" fn call_once(self, _args: T) {
        match self {}
    }
}

impl<T> FnMut<T> for Void {
    extern "rust-call" fn call_mut(&mut self, _args: T) {
        match *self {}
    }
}

impl<T> Fn<T> for Void {
    extern "rust-call" fn call(&self, _args: T) {
        match *self {}
    }
}

Затем переместим Struct::new в другой impl блок:

impl<T> Struct<T, Void> {
    fn new(value: T) -> Struct<T, Void> {
        Struct {
            func: None,
            value: value,
        }
    }
}

Этот impl не универсальный для F: new всегда будет генерировать только Struct где F = Void. Это позволяет избежать двусмысленности в случае, когда func никогда не вызывается.

Наконец, нам нужно заставить func изменить тип строителя:

impl<T, F0: Fn(T)> Struct<T, F0> {
    fn func<F1: Fn(T)>(self, func: F1) -> Struct<T, F1> {
        Struct {
            func: Some(func),
            value: self.value,
        }
    }
}

Этот метод должен оставаться в блоке impl, который является общим для параметра типа F на Struct<T, F>, чтобы его можно было использовать в построителе, на котором уже был вызван func. Однако func также должен быть универсальным, чтобы он мог принимать любой тип функции (а не функцию, соответствующую типу построителя). Затем, вместо того, чтобы изменять self, мы должны создать новый Struct, потому что мы не можем просто преобразовать Struct<T, F0> в Struct<T, F1>.

person Francis Gagné    schedule 19.05.2016