Уникальные фишки: единицы измерения и 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 и исследования данных в скриптах.
Проверьте себя
1. Что проверяют единицы измерения в F#?
AСкорость кода
BСогласованность физических величин на этапе компиляции
CОрфографию
DРазмер памяти
2. Что произойдёт при попытке сложить float<m> и float<s>?
AНичего, сложатся как числа
BОшибка компиляции из-за разных размерностей
CРезультат в float<ms>
DИсключение в рантайме
3. Какова стоимость единиц измерения в рантайме?
AСущественная
BНулевая — они стираются после проверки типов
CЗависит от платформы
DОни хранятся в каждом числе
4. Что делает type provider?
AУскоряет рантайм
BГенерирует типы F# из внешней схемы данных на этапе компиляции
CЗаменяет компилятор
DСоздаёт случайные числа