Одной из отличительных особенностей языка F#, по сравнению с более распространёнными языками программирования, является мощный и всеобъемлющий автоматический вывод типов. Благодаря ему в программах на F# вы почти никогда не указываете типы явно, набираете меньше текста, и получаете в итоге более краткий, фантастически элегантный код. Алгоритмы автоматического вывода типов — захватывающая тема; за ними стоит интересная и красивая теория. Сегодня мы рассмотрим один любопытный аспект автоматического вывода типов в F#, который, возможно, даст вам представление о том, какие сложности возникают в хороших современных алгоритмах такого рода, и, надеюсь, объяснит один камень преткновения, с которым время от времени сталкиваются F#-программисты. Нашей темой сегодня будет ограничение на значения. На MSDN есть хорошая статья на тему ограничения на значения и автоматического обобщения типов в F#, которая даёт очень разумные практические советы в том случае, если вы столкнулись с ним в вашем коде. Однако эта статья просто сухо констатирует: "Компилятор выполняет автоматическое обобщение только на полных определениях функций с явно указанными аргументами и на простых неизменяемых значениях", и не даёт этому практически никаких объяснений, что вполне справедливо, потому что MSDN — просто справочный материал. Данный пост сфокусирован на рассуждениях, лежащих в основе ограничения на значения — я буду отвечать на вопрос "почему?", а не "что делать?". Автоматическое обобщение — мощная возможность автоматического вывода типов F#. Определим простую функцию — например, функцию тождества:  Здесь нет явных аннотаций типов, но компилятор F# выводит для этой функции тип 'a -> 'a: функция принимает аргумент некоего типа и возвращает результат точно такого же типа. Это не особенно сложно, но заметьте, что компилятор F# вывел неявный тип-параметр 'a для функции id. Мы можем скомбинировать функцию id с функцией List.map, которая сама по себе полиморфна: (Не очень полезная функция, но сейчас важнее наглядность). Компилятор F# даёт функции listId корректный тип 'a list -> 'a list — снова произошло автоматическое обобщение. Поскольку List.map — каррированная функция, у нас может возникнуть искушение отбросить аргумент l слева и справа: Но F# компилятор неожиданно возмущается: Что происходит? Статья на MSDN предлагает 4 способа исправить let-определение, которое отвергается компилятором из-за ограничения на значения: Первый способ не для нас — мы хотим, чтобы функция listId была полиморфной. Второй способ вернёт нас к явному указанию параметра list — и это канонический способ определения функций вроде listId в языке F#. Третий способ применим, когда нужно определить что-то, не являющееся функцией, в нашем случае это даёт неубедительный вариант: В рабочем коде я бы воспользовался вторым вариантом решения и оставил параметр функции явным. Но ради обучения давайте попробуем "редко используемый" четвёртый способ: Это компилируется и работает так, как и ожидалось. На первый взгляд кажется, что это ошибка вывода типов — компилятор не может определить тип, поэтому мы добавили аннотацию, чтобы ему помочь. Но подождите, компилятор почти вывел этот тип — он же упоминается в сообщении об ошибке (с таинственной ти́повой переменной '_a)! Будто бы компилятор был ошарашен конкретно этим случаем — почему? Причина вполне разумна. Чтобы увидеть её, давайте рассмотрим другой случай ограничения на значения. Эта ссылочная ячейка на список не скомпилируется: Давайте обойдём это с помощью явных аннотаций типов: Компилятор доволен. Давайте попробуем присвоить v какое-нибудь значение: Правда же, v теперь ссылается на список с единственным элементом 1? Ой! Содержимое v — пустой список! Куда делся наш ? Вот что произошло. Наше присваивание на самом деле может быть переписано так: Левая сторона этого присваивания — это применение v к аргументу типа int. А v, в свою очередь, это не ссылочная ячейка, а функция типа: получив на входе аргумент типа, она вернёт ссылочную ячейку. Наше выражение создаёт новую ссылочную ячейку и присваивает ей . Аналогично, если мы явно укажем аргумент типа в разыменовании v: мы увидим, что v снова применяется к аргументу типа, и возвращает свежую ссылочную ячейку, содержащую пустой список. Чтобы конкретизировать разговор о функциях типа, давайте изучим полученный IL-код. Если мы скомпилируем определение v, наш верный Reflector покажет нам, что v это: То, что мы воспринимаем как значение в F#, на самом деле является обобщенным методом без параметров в соответствующем IL-коде. И присваивание, и разыменование v вызывают IL-метод, который будет каждый раз возвращать новую ссылочную ячейку. Однако же, ничто в выражении не намекает на подобное поведение. Имя v выглядит как обыкновенное значение, а вовсе не метод и даже не функция. Если бы подобное определение было разрешено, F#-разработчиков ожидал бы неприятный сюрприз. Вот поэтому здесь компилятор перестаёт выводить полиморфные параметры — ограничение на значения оберегает вас от неожиданного поведения. Итак, когда безопасно автоматическое обобщение? Сложно привести точные критерии, но напрашивается один простой ответ: обобщение безопасно, когда правая часть let-выражения одновременно: Действительно, причудливое поведение v возникает из-за изменяемости ссылочной ячейки; именно потому, что ссылочная ячейка изменяема, нам было важно, будет ли получена одна или разные ячейки в результате разных обращений к v. Если правая часть let-выражения не содержит побочных эффектов, мы знаем, что всегда получаем эквивалентные объекты, а так как они неизменяемы, нас не волнует, получаем ли мы одну и ту же или различные их копии в результате различных вызовов. С точки зрения компилятора трудно, даже невозможно точно установить, выполнены ли вышеупомянутые условия. Поэтому компилятор использует простое и грубое, но естественное и понятное приближение: он обобщает определение только тогда, когда может вывести чистоту и неизменяемость из синтаксической структуры выражения в правой части let. Поэтому выражение, для которого оригинальное определение let listId l = List.map id l является синтаксическим сахаром обобщается — в правой части создание замыкания; создание замыкания не содержит побочных эффектов и замыкания неизменяемы. Аналогично с размеченными объединениями: и неизменяемыми записями: Здесь r1 получает тип 'a list r. Однако если вы попытаетесь проинициализировать какие-либо поля неизменяемой записи результатом вызова функции:  то значение f не будет обобщено. В примере выше gen — это безусловно грязная функция; она могла бы быть чистой, но компилятор не может об этом знать, поэтому он из предосторожности возвращает ошибку. По этой же причине не обобщается — компилятор не знает, чистая функция List.map или нет. Выражения, для которых компилятор на уровне синтаксиса может определить, что они чистые и возвращают неизменяемые объекты, называются синтаксическими значениями. Так ограничение на значения получило своё название — автоматическое обобщение правой части let-выражения ограничено синтаксическими значениями. Описание языка F# содержит полный список синтаксических значений, но наше обсуждение даёт представление о том, что это за выражения — чистые и возвращающие неизменяемые объекты. Задача, которую мы здесь решаем, не нова — все компиляторы языков семейства ML используют ограничение на значения в той или иной форме. Уникальной же, на мой взгляд, особенностью F# является возможность обойти ограничение на значения с помощью явных аннотаций типов, и это безопасно с точки зрения семантики F#. Когда это может быть полезно? Классический пример — lazy и lazy list. Типичное определение lazy (давайте притворимся, что его нет в нашем языке) на первый взгляд, полно побочных эффектов; компилятору не известен контракт между create и force. Если мы построим lazy list обычным способом с помощью определения lazy и попытаемся определить пустой ленивый список: ограничение на значения не позволит нам это сделать; однако полиморфное использование ленивого списка абсолютно законно; мы можем заявить об этом, явно указав параметр полиморфного типа: Этого достаточно, чтобы определение empty скомпилировалось, но если мы попытаемся его использовать: компилятор снова возмутится: В самом деле, компилятор знает, что empty — это функция типа, которая не подвергается автоматическому обобщению, так как она не принадлежит множеству синтаксических значений. F#, однако, предоставляет здесь лазейку — мы можем указать атрибут  в определении empty:  Это заставит компилятор считать empty синтаксическим значением, и выражение let l = empty скомпилируется. На самом деле, иногда определения вроде нашего полиморфного v могут быть полезными: Если вы пишете функцию, параметризуемую типами и возвращающую изменяемые объекты или имеющую побочные эффекты, укажите атрибут RequiresExplicitTypeArguments: Он полностью соответствует своему названию: теперь вы не можете написать v := [1], только v<int> := [1], и будет понятнее, что происходит на самом деле. Если вы всё это прочувствовали, я надеюсь, что у вас теперь есть чёткое понимание ограничений на значения в F#, и теперь вы можете при необходимости контролировать его с помощью явных аннотаций типов и атрибута GeneralizableValue. Вместе с силой, однако, приходит и ответственность: как правильно сказано в статье на MSDN, эти возможности редко используются в повседневном программировании на F#. В моём F#-коде функции типа появляются только в случаях, аналогичных lazylist — базовых случаях структур данных; во всех остальных ситуациях я следую советам из статьи на MSDN: Для F# существует множество самоучителей, включая материалы для тех, кто пришел с опытом C# или Java. Следующие ссылки могут быть полезными по мере того, как вы будете глубже изучать F#: Также описаны еще несколько способов, как начать изучение F#. И наконец, сообщество F# очень дружелюбно к начинающим. Есть очень активный чат в Slack, поддерживаемый F# Software Foundation, с комнатами для начинающих, к которым вы можете свободно присоединиться. Мы настоятельно рекомендуем вам это сделать! Не забудьте посетить сайт русскоязычного сообщества F#! Если у вас возникнут вопросы по изучению языка, мы будем рады обсудить их в чатах: Автор перевода @AnutaU.
 Перевод и редакторские правки сделаны усилиями русскоязычного сообщества F#-разработчиков. Мы также благодарим @schvepsss за подготовку данной статьи к публикации.