Уникальные фишки: единицы измерения и type providers
Уникальная для F# фишка: прикрепляем к числам единицы измерения, и компилятор ловит смешение величин.
Единицы измерения (units of measure) — аннотации типов для числовых значений, отражающие физические единицы (метры, секунды, рубли); проверяются на этапе компиляции и стираются в рантайме.
Проблема смешения величин
Знаменитая авария аппарата Mars Climate Orbiter случилась из-за путаницы единиц (фунты против ньютонов). В обычном коде float ничего не знает о смысле числа — можно сложить метры с секундами и не заметить. F# умеет это запрещать.
Объявление единиц
Единицу объявляют атрибутом [<Measure>], а затем «приклеивают» к числу через угловые скобки.
[<Measure>] type m // метры
[<Measure>] type s // секунды
let distance = 100.0<m>
let time = 9.58<s>
let speed = distance / time // тип: float<m/s>
printfn "%.2f" (float speed)Вывод:
10.44
Тип speed выведен как float<m/s> — единицы перемножаются и сокращаются по правилам физики автоматически.
Компилятор ловит ошибки
Сложение величин разных размерностей — ошибка компиляции, а не загадочный баг в рантайме.
[<Measure>] type m
[<Measure>] type s
let d = 100.0<m>
let t = 9.58<s>
// let nonsense = d + t // ОШИБКА: нельзя сложить метры и секундыРазмерности должны совпадать для сложения и вычитания; для умножения и деления они комбинируются. Это та же алгебра, что и в физике.
Производные единицы
Из базовых единиц строят составные — например, скорость или площадь.
[<Measure>] type m
[<Measure>] type s
let area = 5.0<m> * 3.0<m> // float<m^2>
let accel = 9.8<m/s^2>
printfn "%.1f %.1f" (float area) (float accel)Вывод:
15.0 9.8
Применение: финансы и наука
Единицы полезны не только в физике: [<Measure>] type USD и [<Measure>] type EUR не дадут случайно сложить доллары с евро без явной конвертации. В финансовых системах (где силён F#) это предотвращает дорогие ошибки.
Как работает под капотом
Единицы измерения существуют только во время проверки типов: компилятор отслеживает их при операциях и проверяет согласованность, но в скомпилированном IL они полностью стираются — остаётся обычный float. Поэтому безопасность бесплатна: ни байта и ни такта накладных расходов в рантайме. Это «фантомные» типы, влияющие только на компиляцию.
Частые ошибки
- Думать, что единицы замедляют код — они стираются и ничего не стоят в рантайме.
- Пытаться напечатать значение с единицей напрямую через
%f— сначала уберите единицу черезfloat. - Складывать разные размерности — это ошибка компиляции (и это хорошо).
Итоги
- Единицы измерения прикрепляют физический смысл к числам:
100.0<m>. - Компилятор проверяет согласованность: сложить метры и секунды нельзя.
- Умножение/деление комбинируют единицы автоматически (
m/s,m^2). - Единицы стираются в рантайме — безопасность без накладных расходов; полезны и в финансах.
Type providers: типизированный доступ к данным
Проблема доступа к внешним данным
Обычно работа с CSV или JSON — это «строки и словари»: обращение по строковым ключам, ручной разбор, ошибки в названиях полей всплывают в рантайме. Хотелось бы, чтобы поля внешних данных были полноценными типизированными свойствами с автодополнением. Это и делают type providers.
Как это выглядит
Подключив пакет FSharp.Data, можно «скормить» провайдеру образец данных, и он сгенерирует типы под его структуру. (Код требует пакета и сети — он только для чтения, не запускается в браузере.)
// требует пакет FSharp.Data
open FSharp.Data
type Stocks = CsvProvider<"date,open,close\n2024-01-01,100.0,105.0">
let data = Stocks.GetSample()
for row in data.Rows do
// row.Open и row.Close — типизированные float, не строки!
printfn "%A: %f -> %f" row.Date row.Open row.CloseПровайдер CsvProvider вывел из заголовка типы столбцов: Date, Open, Close. Теперь row.Open — это float с автодополнением, а опечатка в имени поля не скомпилируется.
JSON и другие источники
Аналогично работают JsonProvider, XmlProvider, провайдеры баз данных (SQL). Схема выводится из образца, и весь дальнейший доступ типобезопасен.
// требует FSharp.Data
open FSharp.Data
type Config = JsonProvider<"""{ "name": "srv", "port": 8080 }""">
let cfg = Config.Parse("""{ "name": "prod", "port": 443 }""")
// cfg.Name : string, cfg.Port : int
printfn "%s:%d" cfg.Name cfg.PortПочему это ценно
- Типобезопасность — поля внешних данных проверяются компилятором.
- Автодополнение — IDE подсказывает доступные поля.
- Меньше шаблонного кода — не нужно вручную писать классы под структуру данных.
Поэтому F# особенно удобен в data science и аналитике: исследовать новый набор данных можно прямо в скрипте .fsx, сразу получая типизированный доступ.
Как работает под капотом
Type provider — это плагин к компилятору. Во время компиляции он обращается к источнику (читает файл-образец, схему БД, описание сервиса) и динамически генерирует типы и их члены, которые компилятор подставляет как обычные. Некоторые провайдеры порождают типы лениво (по мере обращения), чтобы не создавать тысячи классов сразу. В рантайме это уже обычный типизированный код. Магия — на этапе компиляции, не в исполнении.
Частые ошибки
- Ожидать, что type providers — часть стандартной библиотеки: нужен пакет (например,
FSharp.Data). - Полагать, что образец схемы не важен — провайдер выводит типы именно из него.
- Считать это рантайм-рефлексией: типы генерируются на компиляции, доступ типобезопасен.
Итоги
- Type provider генерирует типы F# из внешних данных на этапе компиляции.
- Доступ к полям CSV/JSON/XML/БД становится типобезопасным, с автодополнением.
- Меньше ручного кода-обёртки; опечатки в полях ловятся компилятором.
- Делает F# особенно удобным для data science и исследования данных в скриптах.