Hardcore programmer
1.99K subscribers
9 photos
3 videos
22 links
Продвинутые темы из программирования и computer science. Особенности различных языков программирования. Глубокое погружение в software engineering.

Поддержать канал:
https://www.donationalerts.com/r/hardcore_programmer
Download Telegram
Типизация, типы данных - это то, с чем мы встречаемся практически в любом языке программирования. Языков без типов очень мало, в основном они низкоуровневые, вроде ассемблера, где мы оперируем абстрактными битами, байтами и машинными словами.

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

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

А пока вопрос на затравку:
Что такое сильная (или строгая - strong) и слабая (weak) типизация?
Пишите свои мнения в комментах
Сегодня существует несколько тысяч языков программирования, каждый из которых предлагает свою систему типов. Чтобы анализировать такое многообразие систем, в чём они схожи, а чем отличаются, нужна классификация.

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

В этом посте я предлагаю ознакомиться с некоторыми из таких классификаций:

Статическая vs динамическая типизация.
Рассматривает момент, в который происходит проверка типов - в compiletime или в runtime соответственно. В случае статической типизации типы являются абстракцией компилятора, у каждой сущности есть строго заданный тип и мы можем оперировать ей только в рамках указанного типа. В случае динамической типизации информация о типе хранится рядом со значениями, что позволяет сущностям изменять свой тип в процессе выполнения программы.

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

Номинальная vs структурная.
В номинальной системе типы определяются своим объявлением, полностью идентичные типы объявленные дважды будут разными типами. В структурной системе типы определены своим содержимым, если у двух типов полностью совпадает их структура, то данные типы считаются эквивалентными.

Soundness vs comleteness.
Адекватного русского перевода для этих терминов нет. Данная классификация рассматривает гарантии предоставляемые системой типов. Soundness системы обеспечивают надежность того, что данные не выходят за ограничения своих типов, хотя иногда это приводит к тому, что полностью корректная программа может не пройти проверку типов. Completeness системы относятся к гарантиям типов более либерально, что сильно упрощает написание кода и позволяет некорректной программе пройти проверку типов. Этот подход исходит из мысли, что ничего плохого не случится, если в utf-8 строке окажется нев�лидн�й utf-8 символ.
Так же soundness характеризуют фразой "компилируется - значит работает", а completeness фразой "работает - значит должно компилироваться".

В некоторых источниках так же еще выделяют разделение на implicit и explicit отношение к типам, нередко к тому же путая его с явной/неявной типизацией (трудности перевода в действии). Речь идет про отношение языка к выводу типов, что конечно же не относится к характеристикам самой системы типов. Я рискну утверждать, что вывод типов возможен в абсолютно любой системе типов, как и ничто не мешает в абсолютно любой системе требовать задания типов explicitly.

А что же насчет сильной vs слабой типизации?
Данные термины очень размыты. На просторах интернета можно найти множество определений того, что является сильной или слабой типизацией. Проблема всех этих определений в том, что они являются определениями для любой другой бинарной классификации.
Как жить в таком хаосе? На мой взгляд хорошим подходом будет рассматривать все классификации в комплексе, включая те, которые не являются бинарными. А сами системы рассматривать в градациях, где система типов α может быть сильной по отношению к системе β и слабой по отношению к системе γ.
Статическая типизация сильнее динамической, явная сильнее неявной, номинальная сильнее структурной, soundness сильнее completeness, линейные типы сильнее аффинных и т.д.
На моем youtube вышло новое видео про асимптотическую сложность алгоритмов
Не могу не поделиться этой новостью, ибо я ждал этого несколько лет!

На прошлой неделе вышел релиз Rust 1.74.0, в котором добавили секции lints и workspace.lints в Cargo.toml
Наконец можно конфигурировать линты глобально на весь проект.

Как это было раньше?
Каноничным способом было прописывать атрибуты forbid/deny/warn/allow в корневом модуле каждого 😡 крейта или управлять этим через переменную окружения RUSTFLAGS, что было крайне не удобно.

Альтернативным способом была установка cargo cranky, который хоть и решал проблему, но плохо интегрировался с другим инструментарием Rust.

Релизы Rust выходят каждые 6 недель, но действительно значимые улучшения происходят далеко не в каждом из них.
Иногда система типов может предложить интересные решения дающие больше контроля над типизированными сущностями. В этом посте я хочу рассмотреть 2 таких решения, которые сосредоточены на управлении ресурсами посредством системы типов, а именно линейных и аффинных типах.

Линейные типы описывают объекты, которые должны быть использованы строго 1 раз. Это означает, что получив сущность линейного типа мы обязаны ей воспользоваться, а воспользовавшись однажды сделать это второй раз не выйдет. Если же нарушить эти правила, то программа не пройдет проверку типов.
Попробовать линейные типы можно в языках Haskell (пока что включив расширение компилятора) и Idris 2.

Аффинные типы описывают объекты, которые могут быть использованы не более одного раза. Отличие от линейных типов здесь в том, что у нас нет обязательства использовать сущность аффинного типа.
На аффинных типах построена концепция владения в языке Rust. Все типы в Rust являются аффинными по умолчанию, при передаче по значению используется move семантика, а корректность ее использования (невозможность использовать перемещенное значение) возлагается на проверку типов.
Однако в Rust есть 2 послабления касательно аффинных типов:
- тип может быть промаркирован трейтом Copy, что отключает для него move семантику (при передаче по значению происходит копирование) и делает такой тип обычным;
- дополнительно вводится концепция заимствования через ссылки, что позволяет обращаться к сущности многократно, правда с рядом ограничений.

В целом линейные и аффинные типы позволяют языку вводить дополнительные гарантии управления ресурсами на уровне системы типов. Линейные типы так же дают дополнительную гарантию освобождения ресурсов, тогда как в случае аффинных типов такой гарантии нет, хотя она и может быть переложена на другие механизмы языка (Rust например дает такую гарантию за счет идиомы RAII).
Хочу собрать немного статистики.
Сколько языков программирования вы знаете (на уровне не меньше чем "могу что-то написать для себя")?
Anonymous Poll
25%
1
27%
2
16%
3
7%
4
3%
5
1%
6
2%
7 или больше
12%
ни одного
8%
посмотреть результаты
Полиморфизм - какая первая ассоциация всплывает у вас, когда вы слышите это понятие? Вопрос может показаться странным, если не знать на сколько полиморфизм многогранен. Полиморфизм - он не един, его есть несколько видов, многие языки могут включать в себя сразу несколько таких видов. Хотя все эти виды все же имеют одну общую черту - связь с системой типов, а если точнее любой полиморфизм дает возможность иметь сущности, работающие не с конкретными типами, а с некоторым множеством типов.

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

Полиморфизм подтипов
Данный вид возникает, когда система типов поддерживает возможность выстраивать типы в иерархии. При этом типы стоящие ниже по иерархии по отношению к типам стоящим выше называют подтипами. Основная идея тут в том, что сущность представленная подтипом так же представлена любым его надтипом (типом стоящим по иерархии выше).
Этот вид полиморфизма часто ассоциируют с ООП (с самым распространенным на сегодня его видом, смотри видео про парадигмы если непонятно о чем речь). Иерархия здесь образуется при наследовании классов (наследник является подтипом базового класса) и при имплементации интерфейсов (имплементатор является подтипом интерфейса).
Однако иерархия может появляться и в других случаях, яркий пример тут система типов в TypeScript, в которой все типы (за исключением разве типа any) выстроены в одну огромную иерархию.

Параметрический полиморфизм
Данный вид дает возможность сущностям языка принимать типы в качестве параметров. Полиморфные сущности в данном виде имеют возможность работать с типами обобщенно. Самый распространенный представитель параметрического полиморфизма - это дженерики.
Вершиной параметрического полиморфизма являются типы высших порядков (HKT - Higher-Kinded Types), при поддержке которых возможно частичное применение параметров типа, а так же возможность передачи частичных типов в качестве параметров. Всю мощь HKT позволяет оценить язык Haskell.

Ad-hoc полиморфизм
Данный вид позволяет сущностям иметь несколько реализаций, при этом конкретная реализация определяется на основе типов в составе сущности. Самым ярким представителем тут являются перегрузки функций, когда мы имеем несколько реализаций для функции, а конкретная реализация выбирается на основе типов аргументов. Важное уточнение, перегрузки в TypeScript относятся к параметрическому полиморфизму, так как у всех перегрузок будет единая реализация. Сюда же можно отнести специализацию шаблонов в C++. Шаблоны в C++ одновременно являются представителем как параметрического так и ad-hoc полиморфизма.

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

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

Простейшим способом вывода типов является задание переменной того же типа, что и тип выражения, которым данная переменная инициализирована. На этом принципе работает auto в C++ и var в Java/C#. На нём же работает вывод типов в TypeScript и некоторых других языках.
auto v = 5 * 2; // выведется int

Литералы 5 и 2 имеют тип int, operator*(int, int) возвращает int - результирующий тип всего выражения, а значит и тип переменной v, которая инициализирована этим выражением.
Такой подход позволяет так же выводить типы для дженериков/шаблонов из выражений переданных в аргументы функции.
function foo<T>(arg: T): void;
foo(10); // T выведется в number
foo('hello'); // T выведется в string

Хотя такой способ достаточно просто реализуется в компиляторе, но у него есть недостатки. Нельзя вывести тип для переменных, которые инициализируются не в момент их объявления. А в сложных системах типов такой вывод может преподносить сюрпризы:
let array = []; // выведется never[]
array.push(10); // ошибка типов


Более продвинутым способом вывода типов является алгоритм Хиндли-Милнера.
В данном способе вывод типов анализирует функцию целиком, строя на её основе систему уравнений, неизвестными в которой являются типы. Затем алгоритм за несколько шагов пытается разрешить эту систему, постепенно замещая неизвестные конкретными типами, вычисленные способом похожим на предыдущий. Вычисления останавливаются, когда больше ничего невозможно вычислить, в результате чего получаются или полностью конкретные типы или могут остаться обобщенные типы.
В случае чистых функций, которые по сути являются выражением из аргументов функции в ее возвращаемое значение оставшиеся обобщенные типы не представляют большой проблемы, мы просто получим обобщенную функцию (дженерик). Данный алгоритм применяется в языках семейства ML (Haskell, OCaml), где он может выводить даже сигнатуру для функции, пусть и зачастую в обобщенном виде.
Однако и данный алгоритм не всесилен, бывает что для некоторых переменных удается вывести лишь обобщенный тип, и если тип данной переменной не получится однозначно связать с типами в сигнатуре, то и не получится вывести обобщенную сигнатуру функции. Обычно компиляторы в этом случае требуют указать тип явно:
let v = "10".parse(); // ошибка компиляции
println!("{:?}", v);

Метод parse у строк в Rust является дженериком по возвращаемому значению (сигнатура fn parse<T>(&self): T), из контекста не понятен тип переменной v и нужно либо указать его явно либо явно задать дженерик у parse.
let v: i32 = "10".parse(); // ok
let v = "10".parse::<i32>(); // ok

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

Вывод типов - достаточно мощный инструмент современных языков, сильно упрощающий написание кода. Он так же может давать некоторые дополнительные возможности, вроде анонимных типов, которые без вывода типов были бы просто невозможны.
Сегодня анонсировали Rust 1.75
Обновиться можно будет на следующей неделе.

Главной фишкой новой версии станет поддержка синтаксиса impl Trait в качестве возвращаемого типа функций объявленных в трейтах:
trait MyTrait {
fn some_function() -> impl SomeTrait;
}

impl MyTrait for MyType {
fn some_function() -> impl SomeTrait {
todo!();
}
}

Ну а так как async fn - это по факту сахар над функциями, возвращающими impl Future, async fn так же будут доступны нативно для трейтов:
async fn f() -> i32 { 10 }
// рассахаривается в:
fn f() -> impl Future<Output = i32> { async { 10 } }
// что позволяет теперь делать так:
trait MyTrait {
async fn f();
}

Анонс конечно описывает некоторые нюансы, но развитие языка - это всегда хорошо.
В следующем году ожидается новая редакция языка (они выходят раз в 3 года), вероятно разработчики языка Rust ещё не раз порадуют нас стабилизацией полезных фич в ближайшие месяцы.

Последние пару недель у меня не хватало времени на контент, надеюсь вы простите мне такое затишье. Но всё же и я сделаю пару анонсов:
- В процессе большая статья про систему типов в TypeScript, надеюсь дописать и опубликовать на этих выходных.
- В процессе сценарий для видео, где расскажу зачем разработчику изучать несколько языков, планирую снять и выпустить его до конца года.
- 4, 5 и 6 января хочу на пробу провести стримы на ютубе, ориентируйтесь на старт в 19:00 мск, будем общаться + буду кодить небольшие штуки с нуля в прямом эфире, а если на первых двух стримах будет хотя бы по 50 зрителей, то на третьем возьму новый для себя ЯП и покажу как можно быстро разобраться в новом языке.
Где-то Новый Год уже наступил, а где-то только на подходе.
В новом году я желаю своим подписчикам счастья, благополучия, мира во всем мире, высоких зарплат и карьерных достижений, прибыльных инвестиционных портфелей!
Продолжайте развиваться и будьте себе на уме.
🥂🍾☃️❄️🎉
Сегодня в 19:00 мск будет ещё один стрим (запись останется, ссылку выложу примерно за пол часа).
В первой части поговорим про то как изучать языки программирования и зачем это нужно.
Во второй части попробую освоить новый для себя язык zig и написать на нём небольшую программку.
В стриме, где я разбирался с языком zig, я упомянул такой термин, как алгебраический тип данных (algebraic data type, ADT). Давайте разберемся, что это такое и зачем нужно.

ADT появились в функциональных языках, таких как Haskell или OCaml, но встречаются и в языках ориентированных на другие парадигмы, например в Rust, Zig и TypeScript.
ADT представляют собой тип-сумму, объединяющий в себе данные разных типов по принципу ИЛИ, что позволяет хранить в одной переменной данные разных типов, но при этом типобезопасно их получать. Основным способом извлечения данных из ADT является pattern matching (сопоставление с образцом), который позволяет проверить, какой вариант данных сейчас находится в переменной с ADT типом.

Давайте посмотрим примеры на различных языках. Пусть у нас есть типы Cat и Dog, а мы хотим объединить их в ADT типе Pet.
На Haskell объявление будет выглядеть так:
data Pet = PetCat Cat | PetDog Dog

А на Rust так:
enum Pet {
Cat(Cat),
Dog(Dog),
}

А вот так на TypeScript:
type Pet = {
kind: 'Cat';
cat: Cat;
} | {
kind: 'Dog';
dog: Dog;
};


Имея переменную такого типа Pet мы можем однозначно определять, что внутри - Cat или Dog и выполнять в зависимости от этого разную логику:
f :: Pet -> IO ()
f (PetCat _) = print "Cat"
f (PetDog _) = print "Dog"

match pet {
Pet::Cat(cat) => println!("Cat: {:?}", cat),
Pet::Dog(dog) => println!("Dog: {:?}", dog),
}

switch (pet.kind) {
case 'Cat':
console.log('Cat', pet.cat);
break;
case 'Dog':
console.log('Dog', pet.dog);
break;
}


ADT - это достаточно мощный инструмент в тех системах типов и языках, где он есть, позволяющий оперировать данными разных типов в рамках одной сущности, при этом сохраняя безопасность с точки зрения типов.
Настало время для серии постов про сети, сетевые протоколы и сетевое программирование.
А начнём мы с рассмотрения сетевой модели OSI.

Работа сетей обеспечивается большим количеством различных протоколов, многие из которых работают поверх других протоколов.
Модель OSI классифицирует сетевые протоколы по 7 уровням:

7. Application (прикладной, уровень приложений).
Оперирует формализованными данными, формат которых задается каждым конкретным протоколом. Обеспечивает общение приложений между собой.
Примеры: HTTP, FTP, WebSocket, SMTP, DNS

6. Presentation (представление).
Оперирует абстрактными данными (наборами байт). Обеспечивает функции преобразования данных (шифрование, сжатие).
Примеры: TLS, SSL

5. Session (сессии, сеансы связи).
Оперирует абстрактными данными + мета данными обеспечивающими постоянные соединения и аутентификацию соединения.
Примеры: TLS (частично), TCP (частично), PAP, L2TP, OpenVPN, WireGuard

4. Transport (транспортный).
Оперирует сегментами или датаграммами. Обеспечивает передачу данных на логическом уровне.
Примеры: TCP, UDP

3. Network (сетевой).
Оперирует пакетами. Обеспечивает логическую адресацию (ip адрес, порт) и маршрутизацию.
Примеры: IPv4, IPv6, ICMP

2. Link (канальный).
Оперирует кадрами. Обеспечивает физическую адресацию (mac адрес).
Примеры: Ethernet, IEEE 802.11 (WiFI), LTE (мобильные сети), PPP, DSL

1. Physical (физический).
Оперирует отдельными битами. Обеспечивает передачу данных по физическим каналам связи.
Примеры: RJ (витая пара), SFP (оптоволокно), USB

Согласно данной модели любые данные проходят все уровни от 7 до 1 при отправке данных и от 1 до 7 при получении. Каждый протокол должен взаимодействовать только с протоколами своего и соседних уровней, что на практике не всегда соблюдается.
Так же при передаче данных через множество сетей (что просто необходимо в том же internet) данные могут многократно переходить между уровнями 1, 2 и 3 (маршрутизация), а иногда и между более высокими уровнями (например при балансировке).
Кроме того, технологии виртуальных сетей (например VLAN или VPN) могут делать программную эмуляцию более низких уровней фактически передавая данные на более высоких.
А границы между уровнями 4, 5, 6 и 7 очень часто оказываются размытыми, например протокол HTTP, находящийся на 7 уровне, может работать прямо поверх протокола TCP, находящегося на 4 уровне.

С уровнями 4, 5, 6 и 7 чаще всего взаимодействует прикладное программное обеспечение. В системной разработке можно столкнуться с уровнями 3 и 4. Но и на уровне 2 так же применяются программные решения при разработке прошивок для сетевых устройств. И лишь уровень 1 является полностью аппаратным.

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

Надеюсь вы простите мне длительное молчание, последние полтора месяца у меня выдались достаточно загруженными. Помимо работы был какой-то нереальный наплыв запросов на моё менторство и вечера уходили на проведение занятий. Немного подкосило здоровье, почти каждые выходные катался в клинику, оставил там круглую сумму с 5 нулями, но зато сейчас всё нормализовалось. Так ещё и на работе, помимо основных задач проводил по несколько технических собеседований в неделю. Что-то написать сил просто не оставалось.

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

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

После длительного перерыва возвращаюсь к работе над каналом. Будут некоторые изменения в контенте, за перерыв я многое переосмыслил, о чём так же говорю на записи.
Forwarded from Tina
Бесплатные моковые собеседования, разбор CV и помощь с поиском работы для бэкенд-разработчиков

Знаете это чувство, когда вы тратите кучу времени на поиски работы, но вас нигде не берут и кажется, что крутая работа в IT доступна только избранным? Эту систему можно хакнуть! Для этого нужно:

1️⃣ Научиться правильно презентовать свой опыт в резюме и на собеседовании
2️⃣ Понять, какие вопросы задают рекрутеры на интервью и заранее подготовить ответы на них
3️⃣ Выписать вопросы, которые чаще всего задают на технических собеседованиях и потренироваться отвечать на них
4️⃣ Много практиковаться на реальных или тестовых собеседованиях.

С этими задачами помогут наши партнеры — Solvery — сервис по подбору менторов из IT.
На следующей неделе ребята проводят Backend Fest по трудоустройству! 🚀

Будут не только технические собеседования, но и моковый скрининг с рекрутером, а также разбор CV и вебинар по поиску работы в 2024 году. Их проведут действующие специалисты из Яндекса, Райффайзен Банка, Wildberries и других компаний.

Расписание феста:
13.05 в 18:30 — Разбор CV
14.05 в 19:00 — Моковое собеседование по С++
15.05 в 19:00 — Моковый скрининг с рекрутером
16.05 в 19:00 — Моковое собеседование по Rust
20.05 в 19:00 — Моковое собеседование по Python
21.05 в 19:00 — Моковое собеседование по Java
22.05 в 19:00 — Как искать работу в 2024 году?
23.05 в 19:00 — Моковое собеседование по С#

Вы можете не только прийти зрителем, но и сами пройти тестовое собеседование и разбор резюме – а это очень полезный опыт

Скорее переходите по ссылке и регистрируйтесь, чтобы получить напоминания об эфирах!
Please open Telegram to view this post
VIEW IN TELEGRAM