Это уже 9 часть серии статей по функциональному программированию на F#! Уверен, на Хабре существует не очень много настолько длинных циклов. Но мы не собираемся останавливаться. Сегодня расскажем про вложенные функции, модули, пространства имен и смешивание типов и функций в модулях.  Теперь Вы знаете как определять функции, но как их организовать? В F# возможны три варианта: В этой статье рассмотрим первые два способа, а оставшийся — в следующей. В F# можно определять функции внутри других функций. Это хороший способ инкапсулировать вспомогательные функции, которые необходимы лишь для основной функции и не должны быть видны снаружи. В примере ниже add вложен в addThreeNumbers: Вложенные функции могут получить доступ к родительским параметрам напрямую, потому-что они находятся в её области видимости.
Так, в приведенном ниже примере вложенная функцияprintError не нуждается в параметрах, т.к. она может получить доступ к n и max напрямую. Очень распространённым паттерном является основная функция, определяющая вложенную рекурсивную вспомогательную функцию, которая вызывается с соответствующими начальными значениями.
Ниже приведён пример такого кода: Старайтесь избегать глубокой вложенности, особенно в случаях прямого доступа (не в виде параметров) к родительским переменным.
Чрезмерно глубоко вложенные функции будут столь же сложны для понимания, как худшие из многократно вложенных императивных ветвлений. Пример того как не надо делать: Модуль — это просто набор функций, которые сгруппированы вместе, обычно потому что они работают с одним и тем же типом или типами данных. Определение модуля очень похоже на определение функции. Оно начинается с ключевого слова module, затем идет знак =, после чего следует содержимое модуля.
Содержимое модуля должно быть отформатировано со смещением, также как выражения в определении функций. Определение модуля содержащего две функции: Если открыть этот код в Visual Studio, то при наведении на add можно увидеть полное имя add, которое в действительности равно MathStuff.add, как будто MastStuff был классом, а add — методом. В действительности именно это и происходит. За кулисами F# компилятор создает статический класс со статическими методами. C# эквивалент выглядел бы так: Осознание того, что модули — это всего всего лишь статические классы, а функции — статические методы, даст хорошее понимание того, как модули работают в F#, поскольку большинство правил, применимых к статическим классам, применимо также и к модулям. И так же, как в C# каждая отдельно стоящая функция должна быть частью класса, в F# каждая отдельно стоящая функция должна быть частью модуля. Если есть необходимость обратиться к функции из другого модуля, можно ссылаться на неё через полное имя. Так же можно импортировать все функции другого модуля посредством директивы open, после чего можно будет использовать короткое имя вместо полного. Правила использования имён вполне ожидаемые. Всегда можно обратиться к функции по её полному имени, а можно использовать относительные или неполные имена в зависимости от текущей области видимости. Как и статические классы, модули могут содержать вложенные модули: Другие модули могут ссылаться на функции во вложенных модулях, используя полное или относительное имя в зависимости от обстоятельств: Таким образом, раз модули могут быть вложенными, следовательно, идя вверх по цепочке, можно дойти до некоего родительского модуля верхнего уровня. Это действительно так. Модули "верхнего уровня" определяются несколько иначе, в отличие от модулей, которые были показаны ранее. В общем случае в каждом исходном .FS файле должна существовать декларация "верхнего уровня". Есть кое-какие исключения, но всё равно это хорошая практика. Имя модуля не обязано совпадать с именем файла, но два файла не могут содержать модули с одинаковыми именами. Для .FSX файлов декларация модуля не нужна, в данном случае имя файла скрипта автоматически становится именем модуля. Пример MathStuff, объявленного в качестве модуля "верхнего модуля": Обратите внимание на отсутствие отступа в коде "верхнего уровня" (содержимом module MathStuff), в то время как содержимое вложенного модуля FloatLib всё ещё обязано иметь отступ. Помимо функций модули могут содержать другие объявления, такие как декларации типов, простые значения и инициализирующий код (например, статические конструкторы) Кстати, если вы запускаете данные примеры в интерактивном режиме, вам может понадобиться достаточно часто перезапускать сессию, чтобы код оставался "свежим" и не заражался предыдущими вычислениями. Это снова наш пример модуля. Обратите внимание, что MathStuff содержит функцию add также как и FloatLib. Что произойдёт, если открыть оба модуля в текущей области видимости и вызвать add? А произошло то, что модуль MathStuff.FloatLib переопределил оригинальный MathStuff, который был перекрыт (сокрыт, "shadowed") модулем FloatLib. В результате получаем FS0001 compiler error, потому что первый параметр 1 ожидался как float. Чтобы это исправить, придётся изменить 1 на 1.0. К сожалению, на практике это незаметно и легко упускается из виду. Иногда с помощью такого приёма можно делать интересные трюки, почти как подклассы, но чаще всего наличие одноимённых функций раздражает (например, в случае крайне распространенной функции map). Если вы хотите избежать данного поведения, существует способ пресечь его при помощи атрибута RequireQualifiedAccess. Тот же пример, у которого оба модуля декорированы этим атрибутом: Теперь директива open недоступна: Но по прежнему можно получить доступ к функциям (без какой-либо двусмысленности) через их полные имена: F# поддерживает использование стандартных операторов контроля доступа из .NET, таких как public, private и internal. Статья MSDN содержит полную информацию. Данные спецификаторы доступа являются лишь одним из способов управления видимостью в F#. Совершенно другой способ — использование файлов "сигнатур", которые напоминают заголовочные файлы языка C. Они абстрактно описывают содержимое модуля. Сигнатуры очень полезны для серьёзной инкапсуляции, но для рассмотрения их возможностей придётся дождаться запланированной серии по инкапсуляции и безопасности на основе возможностей. Пространства имён в F# похожи на пространства имён из C#. Они могут быть использованы для организации модулей и типов, чтобы избежать конфликтов имён. Пространство имён, объявленное с помощью ключевого слова namespace: Из-за этого пространства имён полное имя модуля MathStuff стало Utilities.MathStuff, а полное имя add — Utilities.MathStuff.add. К модулям внутри пространства имён применяются те же правила отступа, что были показаны выше для модулей. Также можно объявлять пространство имён явно при помощи добавления точки в имени модуля. Т.е. код выше можно переписать так: Полное имя модуля MathStuff всё ещё Utilities.MathStuff, но теперь это модуль верхнего уровня и его содержимому не нужен отступ. Некоторые дополнительные особенности использования пространств имён: Можно выстраивать иерархию пространств имён, просто разделив имена точками: Так же вы можете объявить два пространства имён в одном файле, если хотите. Следует обратить внимание, что все пространства имён должны быть объявлены полным именем — они не поддерживают вложенность. Конфликт имён между пространством имён и модулем невозможен. Как мы уже видели, модули обычно состоят из множества взаимозависимых функций, которые взаимодействуют с определённым типом данных. В ООП структуры данных и функции над ними, были бы объединены вместе в рамках класса. А в функциональном F# структуры данных и функции над ними объединяются в модуль. Существуют два паттерна комбинирования типов и функций вместе: В первом случае тип декларируется за пределами какого либо модуля (но в пространстве имён), после чего функции, которые работают с этим типом, помещаются в одноимённый типу модуль. В альтернативном варианте тип декларируется внутри модуля и имеет простое название, типа "T" или имя модуля. Доступ к функциям осуществляется приблизительно так: MyModule.Func и MyModule.Func2, а доступ к типу: MyModule.T: Обратите внимание, что в обоих случаях должна быть функция-конструктор, создающая новый экземпляр типа (фабрика). Тогда в клиентском коде почти не придётся обращаться к имени типа явно, и можно будет не задаваться вопросом, находится тип внутри модуля или нет. Итак, какой способ выбрать? Для себя вы можете поэкспериментировать с обоими способами. В случае командной разработки надлежит выбрать один стиль. Если есть множество типов, которые необходимо объявить без каких либо функций, не стоит заморачиваться использованием модуля. Можно объявить типы прямо в пространстве имён, не прибегая к вложенным классам. Например, вы можете захотеть сделать так: А вот другой способ сделать тоже самое. Слово module просто заменяется на слово namespace. В обоих случаях PersonType будет иметь одно и тоже полное имя. Обратите внимание, что данная замена работает только с типами. Функции всегда должны быть объявлены внутри модуля. Для F# существует множество самоучителей, включая материалы для тех, кто пришел с опытом C# или Java. Следующие ссылки могут быть полезными по мере того, как вы будете глубже изучать F#: Также описаны еще несколько способов, как начать изучение F#. И наконец, сообщество F# очень дружелюбно к начинающим. Есть очень активный чат в Slack, поддерживаемый F# Software Foundation, с комнатами для начинающих, к которым вы можете свободно присоединиться. Мы настоятельно рекомендуем вам это сделать! Не забудьте посетить сайт русскоязычного сообщества F#! Если у вас возникнут вопросы по изучению языка, мы будем рады обсудить их в чатах: Автор перевода @kleidemos
 Перевод и редакторские правки сделаны усилиями русскоязычного сообщества F#-разработчиков. Мы также благодарим @schvepsss и @shwars за подготовку данной статьи к публикации. Облачный адвокат