Обновить
19
0.8

Пользователь

Отправить сообщение

Но impl X им является, в чем проблема?

В том что impl X - это не тип а выражение вида какой-то тип реализующий ограничение X. Это не имеет никакого отношения к языкам с наследованием типов где передача типа туда где нужен X означала бы приведение дочернего типа к родительскому. Здесь никакого приведения к X нет, потому что это даже не тип.

То, что есть в Java/C# - это не 10 заповедей, которые всем нужно соблюдать.

Могут ответить аналогичным образом - не нужно везде видеть ООП. Трейты в Rust это по сути классы типов вдохновленные Haskell, синтаксическая схожесть с интерфейсами не делает их таковыми.

В этом суть наследования. Немного контринтуитивно, но верно.

На эти вопросы давно даны ответы как со стороны сообщества:

Why do we need a separate impl for a supertrait

A trait that declares supertraits is merely a trait with constraints, not an extension of its supertraits’ interface.

Conceptually, by declaring trait Tr2: Tr1, you only say that if a type T implements Tr2, it must implement Tr1 as well, but traits Tr1 and Tr2 are otherwise completely independent, and each creates a separate namespace for its associated items (methods, associated types, constants, etc.). Any code using T may import and make use of Tr1, or Tr2, both, or neither; but if it uses only one of them, it should not even have to think about the other existing.

More concretely, by making each trait a separate namespace, Rust avoids the well-known fragile base class problem. Each trait can define its own operations that are guaranteed not to clash with any other if it has not been specifically imported into the namespace. 

Так и с официальной стороны:

If a language must have inheritance to be object oriented, then Rust is not such a language. ...

Inheritance has recently fallen out of favor as a programming design solution in many programming languages because it’s often at risk of sharing more code than necessary. ...

For these reasons, Rust takes the different approach of using trait objects instead of inheritance. Let’s look at how trait objects enable polymorphism in Rust.

Rust doesn't have "inheritance", but you can define a trait as being a superset of another trait.

But because Rust doesn’t have inheritance, we need another way to structure the gui library to allow users to extend it with new types.

В официальных руководствах прописано - нет в Rust нет наследования. Не нужно пытаться его там видеть.

зависит от того что вы им называете. реализация трейтов по сути - наследование интерфейса. Там даже виртуальные таблицы в определённых ситуациях появляются.

Согласен что это вопрос терминологии но все же в Rust нет того что привычно называют наследование в других языках а именно иерархической системы наследования типов.
Вот допустим есть у нас несколько трейтов и структура:

trait Identifiable {
    fn id(&self) -> i32;
}

trait User: Identifiable {
    fn name(&self) -> &str;
}

struct Player {
    id: i32,
    name: String,
}

Теперь мы хотим реализовать User для Player и подразумеваем что User наследует Identifiable и соответственно расширяется всеми его методами и должно быть что-то наподобии:

impl User for Player {
    fn name(&self) -> &str {
        &self.name
    }
    fn id(&self) -> i32 {
        self.id
    }
}
error[E0277]: the trait bound `Player: Identifiable` is not satisfied
  --> src/main.rs:89:15
   |
89 | impl User for Player {
   |               ^^^^^^ the trait `Identifiable` is not implemented for `Player`
   |
help: this trait has no implementations, consider adding one
  --> src/main.rs:76:1
   |
76 | trait Identifiable {
   | ^^^^^^^^^^^^^^^^^^
note: required by a bound in `User`
  --> src/main.rs:80:13
   |
80 | trait User: Identifiable {
   |             ^^^^^^^^^^^^ required by this bound in `User`

Нас наругали, потому что несмотря на то что вроде бы User должен расширяться от Identifiable, это не так, он перенял только ограничения а не его методы. Ну оке премудрости Rust:

impl User for Player {
    fn name(&self) -> &str {
        &self.name
    }
}

impl Identifiable for Player {
    fn id(&self) -> i32 {
        self.id
    }
}

Окей, все работает. Попробуем сделать текст на наследование типов. Опустим момент что структуры никак не могут наследоваться друг от друга, ну допустим у нас только наследование от трейтов работает. Если Player наследуется от User значит вместо любого User можно подставить Player. Проверяем:

fn main() {
    let player = Player {
        id: 1,
        name: "Mike".to_string(),
    };

    let user: User = player;
}
error[E0782]: expected a type, found a trait
   --> src/main.rs:107:15
    |
107 |     let user: User = player;
    |               

Нет нельзя. Но мало того такой код не работает, так еще и компилятор пишет прямо что: нужен тип а найден трейт. Т.е. с точки зрения Rust трейты это вообще не типы.

В сухом остатке имеем:

  • трейты не наследуются друг от друга а только аккумулируют ограничение

  • в между трейтом и структурой нет отношения наследования типов

  • трейт в принципе не является типом

Все встает на свои места если смотреть на это под другим углом. В той же Java или C# интерфейсы - это типы и даже более того классы. Между всеми классами есть отношение наследования благодаря которому наследник перенимает природу родителя как в части данных так и в части поведения. Наследник может быть использован вместо родителя.

Ничего подобного нет в Rust. Есть структуры и есть трейты выступающие контрактами - задача компилятора проверить что типы соответствуют контрактам.

Т.е. Rust решает похожие задачи похожими на вид инструментами но в действительности иным подходом. Можно конечно пытаться натягивать наследование на глобус но мне кажется, в практическом плане, проще смотреть через призму контрактов.

Спасибо за отзыв. Так или иначе, думаю плюсовые шаблоны и эрлановкие clauses можно смело отнести к синтетике на уровне генерации кода.

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

Подскажите пожалуйста, чем был обусловлен выбор именно в пользу Zig а не к примеру Rust?

Мне кажется если заменить Rust на Go/Java/C#? в данной ситуации, то судя по описанию результат был бы таким же.

Очень занимательно это читать, учитывая что автор книги давным давно полюбил ФП и пишет на Clojure:

Then, in 2010 or so, I bumped into Clojure. I had just recently read The Structure and Interpretation of Computer Programs and so was interested in playing around with a LISP derivative.

It has been 11 years now, and I feel no urge to change languages. I reckon that Clojure may be my last programming language. Oh, not that I haven't looked around. I've had some daliances with Golang, Elixr, and Kotlin, and have looked with trepidation at Haskel. I've even played with Scala and F#. I keep looking as new languages arise; but have found nothing that calls me to switch away from Clojure.

также автор книги раньше утверждал что:

The first rule of functions is that they should be small. The second is that they should be smaller than that. (…) Every function in this program was just two, or three, or four lines long. Each was transparently obvious. Each told a story. And each led you to the next in a compelling order. That’s how short your functions should be!

но если зайти к нему на Github то можно увидеть такие функции:

(defn build-character-display [width]
  (let [{:keys [segment-gap segment-length segment-height segment-width height width margin] :as context} (build-context width)
        hseg (build-horizontal-segment context)
        vseg (build-vertical-segment context)
        backslash-seg (build-backslash-segment context)
        slash-seg (build-slash-segment context)
        right-displacement (+ segment-length segment-gap)
        half-segment-width (* 0.5 segment-width)
        half-height (* 0.5 height)
        vertical-displacement (+ segment-height segment-width)
        s0 hseg
        s1 (translate-segment hseg [right-displacement 0])
        s2 vseg
        s3 backslash-seg
        s4 (translate-segment vseg [right-displacement 0])
        s5 slash-seg
        s6 (translate-segment vseg [(- width margin margin segment-width) 0])
        s7 (translate-segment hseg [0 (- half-height margin half-segment-width)])
        s8 (translate-segment hseg [right-displacement (- half-height margin half-segment-width)])
        s9 (translate-segment vseg [0 vertical-displacement])
        s10 (translate-segment slash-seg [(+ (* -0.5 width) half-segment-width margin) (- (* 0.5 height) half-segment-width margin)])
        s11 (translate-segment vseg [(+ segment-length (* 0.5 segment-gap)) vertical-displacement])
        s12 (translate-segment backslash-seg [(- (* 0.5 width) half-segment-width margin) (- (* 0.5 height) half-segment-width margin)])
        s13 (translate-segment vseg [(- width margin margin segment-width) vertical-displacement])
        s14 (translate-segment hseg [0 (- height segment-width margin margin)])
        s15 (translate-segment hseg [right-displacement (- height segment-width margin margin)])]
    {:context context
     :segments [s0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15]}))

https://github.com/unclebob/skillBoard

Автор уже давно сам своим рекомендациям не следует но вы разумеется следуйте.

Но читабельность Perl заканчивалась ровно в тот момент, когда я открывал чужой код на Perl. Можете ли Вы утверждать, что любой код на C# такой же читабельный, как любой код на Go?

Нет, точно также как не могу утверждать что любой код на Go будет такой же читабельный как на C#. Это вообще палка о двух концах. Там где Go будет превосходить в одном месте, из-за этих же качеств будет проигрывать в другом. Уже писал про DSL, там где нужна концентрация на правилах и более высокоуровневой логике а не реализации, Go в хлам проиграет. Другой пример стандартизация. Из-за минимализма, в Go отказались добавлять в стандартную библиотеку и язык многие вещи, которые разработчики вынуждены весосипедить или подключать разношерстные библиотеки. Как итог зоопарк решений вместо единообразия. Разработчики других языков работают с одними и теми же решениями одинаковым образом а в Go работают с зоопарками и велосипедами, где новому разработчику будет проще освоиться на проекте думаю вопрос риторический.
В общем все относительно. Не отрицаю что в Go многое сделано для читабельности но не нужно это гиперболизировать.

Нет, сильная сторона - это как раз ясность и читабельность кода. Да-да-да, внезапно оказалось, что очень простой язык намного проще читать, не смотря на отсутствие синтаксического сахара позволяющего в очень "краткую и наглядную" запись (вроде Вашего примера) и не смотря на "избыточную и многословную" обработку ошибок и кучу boilerplate кода. Такой вот парадокс!

Не нужно считать создателей других ЯП идиотами, которые не догадались вернутся в 80-десятые за читабельностью. Любое достоинство вытекает из недостатка и Go не исключение, то что в одном случае будет читабельно и хорошо, в другом превратиться в тыкву. За примером ходить далеко не нужно, любая предметная область где можно зарулить с DSL на Go будет вызывать кровь из глаз. Потому что нельзя победить проблемы просто радикально упоровшись во что-то одно.

У меня недавно был удачный пример, сначала написал приложение на Go, потом по воле обстоятельств пришлось переписать его на C#. Спойлер, чуда не произошло. Проект на C# выглядел заметно приятнее: был минималистичнее, проще и читабельней чем на Go. Это я не про то что Go плохой а скорей про то надоели эти сказки про его ультимативную читабельность.

Забавный факт: после рассмотрения сотен идей и предложений как именно стоит изменить обработку ошибок команда Go спустя 15 лет попыток что-то с этим сделать приняла решение… перестать пытаться! :) Случилось это буквально на днях - пару месяцев назад: https://go.dev/blog/error-syntax. И ключевая причина в том, что до сих пор неясно для чего конкретно нужно изменить обработку ошибок: чтобы стало легче читать/писать логику обработки ошибок, или чтобы стало проще НЕ писать эту логику.

Многие гоферы охренели после этой новости, а многие назвали это бессилием

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

На самом деле скорей нет чем да. Все тоже самое можно получить взяв тот же C# к примеру. Сила Go в минимальных сроках его изучения с одной стороны и не дурных характеристиках - с другой. По итогу для компании - это палочка выручалочка. Можно хантить разрабов с других языков и быстро их переучивать на Go.Скилл некромантии. Чтобы дать бизнесу утилизацию ресурсов памяти и процессора, для начала нужно найти ресурсы в виде разрабов.

Говорят, мир где принято делать все правильно и реальные мир иногда не одно и тоже

Ну и вообще, если вы хотите изучить Go, рекомендую ещё раз подумать и изучить Kotlin).

Kotlin прикольный. Довелось на нем проект написать. Но это по сути джавазаменитель. Да добавили null-безопасность(на которую библиотеки кладут прибор), да более приятный синтаксис и прикольные фичи но... Никаких новых идей или ультимативных киллер-фич, которые бы помогли ответить на вопрос - зачем брать этот язык вместо Java.

Взять тот же Go. Он набрал популярность из-за определенных фич: минималистичность, позволяющая нанимать не только гоферов но и всяких джавистов и очень быстро их переучивать, производительность, решение проблемы асинхронных операций из коробки, на выходе простой бинарник без необходимости морочить голову виртуальными машинами. Это аргументы. А что предлагает Kotlin? Null-safe? Этого мало. Корутины? Так они и так везде есть, так сейчас еще в Java завезли виртуальные потоки. Красивый и удобный синтаксис? Не самый сильный аргумент. Возможность писать все на Kotlin: фронтенд, мобилки, бэкенд и т.д? Хорошая идея, но не взлетело. В немалой степени еще проблема что Kotlin узник JVM и в огромном числе проблема он просто не может перегнать Java. Удел Kotlin по идее - заменить Java, но не смотря не очень существенные успехи, тоже не взлетело в той мере в которой хотелось бы.

По итогу остается послевкусие. Язык то выучен полноценный и время на это потрачено но вот существенной пользы от этого мало.

После Java/C# всем отлично заходит

По джаве да, но и то не всем. По C# большие сомнения. C#/.net с его экосистемой настолько круче Kotlin практически во всем, что я даже не знаю какие мотивы должны быть у человека для того чтобы ему это зашло.

в случае ошибки в Java коде, нам нужно будет смотреть в портянку stack trace и пытаться понять, какая из строчек выдала ошибку.

В Go ошибка сразу покажет, где именно и что сломалось

При всех минусах исключений, вот как раз в место нахождения ошибки там полный порядок. В Go все очень сильно зависит от того насколько удачно ошибка написано. Если вам вернется "number is incorrect" - удачи по всему проекту искать хотя бы примерно откуда это прилетело.

опции не обратить внимание на ошибку — у нас нет

Пытаемся удалить файл которого нет:

filename := "./1.txt"
err := os.Remove(filename)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("File %s is removed", filename)
...
2025/06/23 09:32:05 remove ./1.txt: The system cannot find the file specified.
exit status 1

А что будет если разработчик просто забудет или не увидит что функция возвращает ошибку?

filename := "./1.txt"
os.Remove(filename)
fmt.Printf("File %s is removed", filename)
...
File ./1.txt is removed

В Java строка представляет собой неизменяемую последовательность char.

Уже давно, это последовательность байт а не char.

В Go тип string — это неизменяемый срез байтов (UTF-8 по умолчанию).

It’s important to state right up front that a string holds arbitrary bytes. It is not required to hold Unicode text, UTF-8 text, or any other predefined format. As far as the content of a string is concerned, it is exactly equivalent to a slice of bytes. Rob Pike.

Для сравнения: в декларативном подходе мы бы просто описали, что хотим получить, не указывая, как хотим это сделать. Теперь мы используем метод filter для фильтрации сотрудников по возрасту и зарплате, а затем применяем метод sumOf, чтобы получить сумму зарплат отфильтрованных сотрудников.

Это распространенное заблуждение которое кочует из статьи в статью про ФП.

    val totalSalary = employees
        .filter { it.age > 30 && it.salary > 50000 }
        .sumOf { it.salary }

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

Декларативный пример это SQL ил html. Где вообще нет прямой связи между описываемым результатом и тем как это будет достигнуто.

Если возможно написать сервис в 100 строк — лучше написать так. Потом, если потребуется, его будет несложно переработать под более удачную архитектуру.

Видно что Вы или редко или не сталкивались с такими задачами. Лично по моему опыту, классическая ситуация, аналитики РП уговорили сделать тяп ляп в 100 строк кода сейчас, потому что сдаем проект через полгода и забываем. На деле, через полтора года - теперь чтобы "переработать под более удачную архитектуру" нужно в 10-15 раз больше времени а исправление багов почему то стало занимать не часы а дни и недели.
Никогда такого не было и вот опять.

Ещё страшнее, когда фанатик одной архитектуры попадает на проект с другой архитектурой и начинает его переписывать под своё чувство прекрасного. За чей счёт? Правильно — за счёт компании.

В целом если это не оправдано то да. Но часто бывает что с прошлой архитектурой работать очень сложно и неудобно. Работа будет малоэффективной, задачу и баги будут делаться долго. За чей счёт?

Например, ООП-разработчики очень любят плодить интерфейс на каждый класс.

А теперь спросите себя насколько создание интерфейса или дописывание в него метода замедляет разработку. На 10-60 секунд? А когда потребуется все таки писать тесты сколько это сэкономит?

Подобное мышление приводит к тому, что простенькие сервисы, которые на каком-нибудь Python или PHP могли бы прекрасно работать и поддерживаться, пишутся на Go или C#. Фанаты этих языков будут с пеной у рта доказывать, насколько они быстрые. Но разработка на них не быстрее, а это — деньги компании. Сколько потеряет компания, если метод будет работать не 100 мс, а 200? Думаю, нисколько.

Сколько потеряет компания когда их мелки сервис разрастется, когда нужно будет куча ресурсов и в конце концов когда все равно все перепишут на на Go или C# и потратят на это огромную кучу денег вместо того чтобы сделать сразу нормально. Сколько компаний с этим столкнулось и столкнутся.
Никогда такого не было и вот опять.

Подобная ситуация легко возникает, если, например, тимлид не даёт разработчикам разнообразные задачи, чтобы они лучше понимали проект в общем, а только узконаправленные.

У Вас противоречие - если тимлид раздает задачи разным людям значит все эксперты в своих областях и рассказываете Вы про человека на котором держится все команда.

В целом со многих согласен. Но...
В статье типичная ошибка - качели в одну сторону. Вы описываете как вредит оверинжиниринг. Но жизнь то вещь сложная и разработка тоже. Нельзя просто "Если возможно написать сервис в 100 строк — лучше написать так ", если бы там все просто было и оверинженерига бы не было а оно рождается из как раз из гипертрофированных попыток исправить тот ад который обычно случается после подобных советов.
Сложность в балансе.

Все это выглядит как лечение симптомов а не причины. Причем лечение симптомов закономерно приводящее к другим проблемам. Начнем с того, что упрощения автора во многих случаях просто делают пресловутую гексагональную архитектуру бесполезной. К примеру, он предлагает делать прямые вызовы репозиториев, при этом не задумываясь зачем вообще репозитории чаще всего спрятаны и к каким проблемам это может привести. Немножко подумаем и поймем что репозиторий можно отдавать только на чтение. Почему? Потому что если мы позволяем модифицировать данные в любом месте проекта то весь смысл слоеной архитектуры, теряется.

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

Другой намного более важный вопрос которым стоит задаться: гексагональная архитектура - это вообще корректный подход к разработке плюсы которого перевешиваю минусы? Если немного отстранится от хайпа и карго культов, мы имеет сильно овер-инженерный подход, который даже "по учебнику" получается не очень и предлагается интерпретировать все правила вольным для каждого разработчика образом походу теряя те преимущества которые данный подход обещал. Может быть немного притормозить и оглянутся по сторонам?

В Java всегда можно посмотреть, например, полный стектрейс и найти, в чём именно ошибка.

А если ошибки нет и просто что-то не работает не подавая виду не нужно будет SpringBoot по запчастям разбирать чтобы понять что не так попутно гадая на картах?

А несовместимость версий в рантайме выясняется легко при первом запуске.

Видно не сталкивались с проблемами которые легко не выясняются.

Что касается написания методов в разном стиле: с точки зрения производительности и бизнес-логики как будто разницы нет, но ведь каждый разработчик, при очередных правках в этом участке кода, будет стараться переписать его в своём стиле. Может даже и неосознанно. Что приводит к повышению вероятности ошибок и усложняет ревью. А ценности для конечного пользователя мы тут не привносим никакой.

А то что мы вместо того чтобы писать решение занимается бюрократией с вызовом расово правильных видов синтаксиса это мы так ценность вносим?

И в этом основная сила Java, потому что разработчики не тратят время на то, чтобы выработать общий стандарт оформления кода, что критично для масштабных корпоративных проектов.

Но, есть и обратная сторона. В той же Java из-за многословности разработчики понапридумывали костылей. Ладно Lombok если более менее на кодогенерации работает и предсказуем. Но огромная часть Java это дикое количество АОП. И вроде бы язык не самый вариативный а приложения порой работают также непредсказуемо как на JS, когда в рантайме выясняется что нет какого-то класса потому что версии библиотек не сошлись. Одного языка мало, нужна еще и адекватная экосистема.

Если в команде больше одного разработчика, уже есть риск, что они будут писать методы в разном стиле. То есть даже в таком банальном примере возникает необходимость в выработке единого стиля, а также контроль за выполнением этих договорённостей на code review.

Простой вопрос - зачем? Единственное что приходит на ум это то что разработчики не знают язык на котором пишут, ну тогда и меры другие нужно принимать наверное.

История развития многих языков программирования показывает, что чем сильнее мы можем задать ограничения в коде — тем проще потом контролировать корректность его работы и искать баги.

Если бы было так то все бы писали на Haskell или Rust.

НЛО прилетело и опубликовало эту надпись здесь

Я так до конца и не понял статья шуточная или на полном серьезе.

Информация

В рейтинге
1 860-й
Откуда
Санкт-Петербург и область, Россия
Зарегистрирован
Активность

Специализация

Бэкенд разработчик
Ведущий
C#
Java
Rust
Golang
Многопоточность
C
Системное программирование
Разработка игр
Unity3d
Алгоритмы и структуры данных