От TupleStruct к структуре с функцией

Я попытался реорганизовать этот код:

let picture = (|x:Float, y:Float| sin(x)*x/2.0 + x — y, X as f32, Y as f32);

В использовании структуры.

Мой код на N-й итерации, который все еще не работает:

type RootFunc = Fn(f32, f32) ->f32 ;
struct Picture{
    func: RootFunc,
    float_x: f32,
    float_y: f32
}

Ошибка, которую я получаю, разумна, но мой мозг недостаточно велик, чтобы обработать всю проблему.

--> src/main.rs:29:5
   |
29 |     func:RootFunc,
   |     ^^^^^^^^^^^^^ doesn't have a size known at compile-time

Лирическая часть

Мой процесс «изучения ржавчины» состоит из трех режимов: трепет (когда я обнаружил, что Rust действительно быстр и помогает мне избегать ошибок); разочарование в знаниях (когда я не могу заставить простую вещь работать, в основном с графическим стеком или библиотеками) и полная беспомощность, когда я натыкаюсь на Serious Stuff в Rust.

Разочарование в лоре - это нормально, я привык к нему на работе. Это то, как вы узнаете что-то на собственном горьком опыте.

Но эти серьезные вещи для меня в новинку. Старые языки, с которыми я знаком, так легко объяснить, что большую часть времени любая проблема языкового уровня сводится к нескольким минутам размышлений и чтения. После этого вас либо поймут, что вы ошиблись, либо поняли, почему это необходимо, либо, в редких случаях, вы разочаровались в дизайне языка.

Но Rust толкает меня в какое-то новое нечестивое царство, когда мне приходится ломать и сдавливать свой мозг сверх точки пластичности (вызывая необратимые изменения).

Вся ментальная модель Rust настолько далека от обычной тривиальности, что просто не вписывается в рабочее пространство моего мозга. Это настоящее обучение?

В любом случае, вернемся к проблеме.

Рассуждения об ошибке

Когда у меня был анонимный TupleStruct с функцией, все было просто: есть функция и два числа с плавающей запятой. Объедините их вместе, и вы получите свой TupleStruct. Тип бетона с заданной функцией рассчитывается автоматически.

Когда я попытался преобразовать его в правильную структуру со связанными функциями, я попросил Rust создать что-то, что может работать с «любой функцией», которая принимает два числа с плавающей запятой и возвращает одно.

В чем проблема? Проблема в том, что Rust не может угадать размер «произвольной функции» во время компиляции.

Почему? Моя текущая идея, что размер функции - это размер «тела функции». Один лайнер имеет другой размер, чем сто лайнер.

Давай проверим это.

use std::mem::size_of_val;
  
fn main() {
    let x = & (|x: f32, y: f32|{ x + y}, 0 as f32, 0 as f32);
    let y = & (|x: f32, y: f32|{ x + y + 9.0}, 0 as f32, 0 as f32);
    println!("{}", size_of_val(x));
    println!("{}", size_of_val(y));
}

Оба дают 8. Это противоречит моей теории (что кортеж включает тело функции). Первый кортеж прекрасно живет с размером два f32 (32 + 32), а второй просто игнорирует дополнительную константу (9), поэтому тело не считается.

Итак, почему у меня не может быть структура с произвольной функцией в ней? В моих (N-1) попытках мне удалось скомпилировать его с помощью этого:

type RootFunc = Fn(f32, f32) ->f32;
struct Picture{
    func:&'static RootFunc,
    float_x: f32,
    float_y: f32
}

Он компилируется, но его ударили следующим образом:

warning: trait objects without an explicit `dyn` are deprecated
  --> src/main.rs:26:17
   |
26 | type RootFunc = Fn(f32, f32) ->f32 ;
   |                 ^^^^^^^^^^^^^^^^^^ help: use `dyn`: `dyn Fn(f32, f32) ->f32`

Я не хочу использовать здесь dyn трейты, так как это косвенный вызов указателем (собственно, это то, что я написал - хранить указатель на функцию).

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

Да и как Rust это умеет? Только с дженериками, правда? Если вам нужна мономорфизация, вы используете дженерики. Надеюсь, это была хорошая причина.

Общая структура

Проблема здесь в том, что я хочу, чтобы функция была универсальной. Это означает структуру с параметром типа.

struct Picture<F>{
    func: F,
    float_x: f32,
    float_y: f32
}

Мне не нравится, что здесь он слишком общий, поскольку я не ограничивал букву «F».

Я могу это сделать? (Я перейду к его реализации, как только напишу удовлетворительную структуру).

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

struct Picture<F: Fn(f32,f32)-> f32>{
    func:F,
    float_x: f32,
    float_y: f32
}

Компилятор принял его без нареканий.

Реализация конструкции

impl Picture<F: Fn(f32,f32)-> f32>{
    fn new<F>(func:F, float_x: f32, float_y: f32){
        Picture{func:F, float_x:float_x, float_y:float_y}
    }
}

И ад открывается на свободу.

error[E0658]: associated type bounds are unstable
  --> src/main.rs:42:14
   |
42 | impl Picture<F: Fn(f32,f32)-> f32>{
   |              ^^^^^^^^^^^^^^^^^^^^
   |
   = note: for more information, see https://github.com/rust-lang/rust/issues/52662
error[E0107]: wrong number of type arguments: expected 1, found 0
  --> src/main.rs:42:6
   |
42 | impl Picture<F: Fn(f32,f32)-> f32>{
   |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected 1 type argument
error[E0229]: associated type bindings are not allowed here
  --> src/main.rs:42:14
   |
42 | impl Picture<F: Fn(f32,f32)-> f32>{
   |              ^^^^^^^^^^^^^^^^^^^^ associated type not allowed here

Я сделал что-то ужасное здесь. Я призвал высших демонов, у меня нет разрешения вредить, я умру.

Я не это имел в виду.

Я хотел сказать, что это реализация общей структуры. Хорошо, может быть, параметры типа более высокого порядка (я подозреваю, что это имя демона, которого я случайно вызвал) - неправильная идея.

Попробуем избежать параметра типа для impl.

impl Picture{
    fn new<F>(func:F, float_x: f32, float_y: f32)
        where F: Fn(f32, f32)-> f32{
            Picture{func:func, float_x:float_x, float_y:float_y}
    }
}

Но тут возникает простая ошибка:

42 | impl Picture{
   |      ^^^^^^^ expected 1 type argument

Значит, я должен передать тип в impl, не так ли?

Как?

Я просматриваю главу в Rust Book. У них есть это:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

Давай сделаем это…

impl<F> Picture<F>{
    fn new(func:F, float_x: f32, float_y: f32) -> Self
    {
            Picture{func:func, float_x:float_x, float_y:float_y}
    }
}

(да, про возвращаемое значение я забыл в прошлый раз)

Теперь Rust становится полезным:

28 | struct Picture<F: Fn(f32,f32)-> f32>{
   | ------------------------------------ required by `Picture`
...
42 | impl<F> Picture<F>{
   |      -  ^^^^^^^^^^ expected an `Fn<(f32, f32)>` closure, found `F`
   |      |
   |      help: consider restricting this bound: `F: std::ops::Fn<(f32, f32)>`
   |

Давай сделаем, как сказано:

impl<F: Fn(f32,f32) -> f32> Picture<F>{
    fn new(func:F, float_x: f32, float_y: f32) -> Self
    {
          Picture{func:func, float_x:float_x, float_y:float_y}
    }
}

Наконец, он скомпилирован. Более того, я понимаю большинство ошибок, которые у меня были.

Это было препятствием. (Тяжело и больно).

Пост-мышление

Проблема, с которой я столкнулся в 1 .. (N-1) попытках, заключалась в том, что мне нужно было объявить <F> в двух местах: в impl<F> и в Picture<F>.

Почему? Я попытаюсь изложить свою логику, но есть вероятность 99%, что я ошибаюсь.

Первый F (impl<F>) - это объявление параметра типа для impl. Мы заявляем, что impl является универсальным, например он может работать для нескольких различных F (функций в нашем случае), поэтому каждое новое использование new функции из этого impl будет производить мономорфный код.

Второе использование «F» (Picture<F>) - это ИСПОЛЬЗОВАНИЕ этого типа. Мы говорим здесь, что реализуем код (все функции в теле) для структуры с заданным параметром типа F. По сути, мы передаем «F» в качестве параметра в структуру.

… Дай мне это проверить.

struct Picture<F: Fn(f32,f32)-> f32, G>{
    func:F,
    func2: G,
    float_x: f32,
    float_y: f32
}

impl<F: Fn(f32,f32) -> f32,> Picture<F,F>{
    fn new(func:F, func2:F, float_x: f32, float_y: f32) -> Self
    {
            Picture{func:func, func2:func2, float_x:float_x, float_y:float_y}
    }
}

Это действительно сложно произнести по буквам ...

  1. Мы объявляем универсальную структуру с двумя параметрами типа: F (ограниченный функцией и G - свободный параметр).
  2. Мы объявляем реализацию этой структуры, когда F - функция (f32, f32->f32), а G - функция того же типа.

По сути, мы создаем экземпляр Picture - он не обеспечивает границ для G, но предоставленная нами реализация поддерживает только ситуации, когда func2 имеет тот же тип, что и F. Поскольку мы знаем все, что нам нужно для F, этот код можно компилировать, поскольку G то же самое, что F. Если G отличается, то для этой ситуации нет реализации. Надеюсь, я правильно понял.

Последняя часть головной боли, которую я навязываю себе. Могу ли я иметь два имп с разным набором границ? Я думаю, что не могу. Я пробовал, но Руст меня ругает:

duplicate definitions for `new`

(Даже когда у меня были разные impl разделы). Я действительно на грани возможностей моего мозга, поэтому я просто принимаю, что не могу, пытаясь обобщить, почему.

Последний кусок на сегодня

Я собираюсь реорганизовать код, чтобы использовать общую структуру вместо TupleStructure…

Я сделал это. Несколько опечаток, но я знаю, что делаю. На сегодня это был самый сложный коммит. Надеюсь, я изучил некоторые важные аспекты Rust. Изучить Rust - ТРУДНО. Пользоваться - одно удовольствие, а вот учиться… ох, это сложно!

(Кстати, мне нравится этот пост, потому что я смог отчасти уловить огромное истончение, которое стоит за некоторыми тривиально выглядящими изменениями в git).