От 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} } }
Это действительно сложно произнести по буквам ...
- Мы объявляем универсальную структуру с двумя параметрами типа: F (ограниченный функцией и G - свободный параметр).
- Мы объявляем реализацию этой структуры, когда F - функция (
f32, f32->f32
), а G - функция того же типа.
По сути, мы создаем экземпляр Picture
- он не обеспечивает границ для G
, но предоставленная нами реализация поддерживает только ситуации, когда func2
имеет тот же тип, что и F
. Поскольку мы знаем все, что нам нужно для F
, этот код можно компилировать, поскольку G
то же самое, что F
. Если G
отличается, то для этой ситуации нет реализации. Надеюсь, я правильно понял.
Последняя часть головной боли, которую я навязываю себе. Могу ли я иметь два имп с разным набором границ? Я думаю, что не могу. Я пробовал, но Руст меня ругает:
duplicate definitions for `new`
(Даже когда у меня были разные impl
разделы). Я действительно на грани возможностей моего мозга, поэтому я просто принимаю, что не могу, пытаясь обобщить, почему.
Последний кусок на сегодня
Я собираюсь реорганизовать код, чтобы использовать общую структуру вместо TupleStructure…
Я сделал это. Несколько опечаток, но я знаю, что делаю. На сегодня это был самый сложный коммит. Надеюсь, я изучил некоторые важные аспекты Rust. Изучить Rust - ТРУДНО. Пользоваться - одно удовольствие, а вот учиться… ох, это сложно!
(Кстати, мне нравится этот пост, потому что я смог отчасти уловить огромное истончение, которое стоит за некоторыми тривиально выглядящими изменениями в git).