
Я как большой фанат Wolfram Language (WL) очень часто изучаю открытые репозитории с кодом на этом языке. Изучив достаточно много кода я заметил, что стиль написания этого кода очень сильно разнится от проекта к проекту. Но так же я изучил много встроенных пакетов в Mathematica/Wolfram Language, которые были написаны разработчиками из Wolfram Research. В большинстве случаев они были написаны еще хуже (т.е. более неструктурированно и без единого стиля) чем пакеты такого же объема и сложности в открытом доступе. Но и среди проектов на GitHub и среди пакетов в языке мне попадались те, которые действительно хорошо написаны. Постепенно у меня сформировалось понимание того стиля, который будет наиболее прост и понятен большинству пользователей WL. В этой статье я хочу поделиться своим мнением и задокументировать тот стиль и ту конвенцию, которую я постепенно выработал для себя. Возможно, это станет еще кому-то полезно и изучив от корки до корки эту статью, а лучше вызубрив, чтобы от зубов отскакивало, вы станете так быстро решать уравнения и строить графики, что...
Системные функции
В Wolfram Language существует огромное количество встроенных функций. И подавляющее большинство имен функций подчиняются следующим правилам:
Все системные функции и переменные начинаются с прописной буквы на латинице
Все системные функции языка записываются в PascalCase
В именах используются символы латиницы и достаточно редко цифры. Символ подчеркивания синтаксис не поддерживает
Еще одно важное соглашение - все встроенные функции представляют собой набор из полных слов английского алфавита и должны легко читаться
Большинство классических функций по своему смыслу являются глаголами, которые обозначают действие
Сокращенные или укороченные имена используются только для математических функций, которые имеют устоявшиеся в математике обозначения
Еще одно правило касается функ��ий, которые называются именами ученых - чаще всего такие функции имеют префикс в виде имени ученого, а далее идет стандартное принятое обозначение этой функции.
Общепринятые аббревиатуры используются как есть, если встречаются в именах, например URL, HTTP.. в отличии, например, от C#, где аббревиатура URL в именах классов и методов пишется как
Url.
Ниже примеры имен встроенных функций:
Plot (*строит аналитический график*) Solve (*аналитически решает уравнение*) FindRoot (*численно ищет корни уравнения*) Replace (*заменяет части выражений*)
И пример функций с устоявшимися в математике именами:
Sin (*синус*) Cos (*косинус*) Tan (*тангенс*) Cosh (*гиперболический косинус*) BesselJ (*функция Бесселя*) BesselY (*эту чаще называют функцией Неймана. Небольшая несостыковка*) EulerGamma (*гамма-функция Эйлера*) URLRead (*вызывает cURL*)
Как видно из примеров, даже устоявшиеся в математике имена функций все равно начинаются с большой буквы.
Встроенные переменные
В Wolfram Language существует два типа встроенных переменных или символов. Во-первых, это могут быть константы или символы с отложенным вычислением. Тогда они подчиняются тем же правилам, что и встроенные функции, то есть PascalCase в виде полных английских слов, если для этого символа нет устоявшегося сокращения. Единственное отличие в том, что по смыслу это обычно не действие. Вот пример таких констант и символов:
Pi (*число пи*) I (*мнимая единица*) E (*экспонента*) Today (*сегодняшняя дата*) Now (*текущее время*) Red (*переменная хранящий красный ��вет*) Large (*символ без значения нужен для стилизации*)
Второй вид встроенных переменных - это системные константы. Их принято записывать с префиксом в виде $:
$MachinePrecision (*машинная точность*) $MachineEpsilon (*машинная погрешность*) $HistoryLength (*размер истории вычислений*) $Context (*текущий контекст*) $CloudCreditsAvailable (*число доступных в облаке кредитов*)
Как видите, в основном такие переменные связаны с самим языком, системой и их нельзя отнести к какой-то бизнес области или научной области применения. А так же в таких константах и глобальных системных переменных может храниться информация о среде выполнения или текущей сессии.
Интерактивный режим
То, в каком стиле писать код на Wolfram Language зависит от того, что вы делаете. Рабочий процесс разработки пакетов, скриптов и создания интерактивных документов отличается.
Режим работы в интерактивных документах похож на то, как происходит разработка в Jupyter Notebook (хотя если быть честным, то это Jupyter Notebook похож на Mathematica, так как изначально IPython, как предшественник Jupyter был разработан под влиянием Mathematica). В интерактивных блокнотах можно создавать ячейки с кодом, текстом и форматированием, выполнять ячейки с кодом и тут же видеть результат. Поэтому в интерактивных блокнотах могут быть:
Ячейки с длинными последовательностями команд, которые можно назвать скриптами
Создание переменных
Создание собственных функций
Вызов встроенных и собственных функций
Рекомендуется всегда придерживаться одного очень простого правила - одно логическое действие - одна ячейка, то есть писать множество функций, тут же их вызывать, создавать переменные и что-то рисовать в одной ячейке - это плохой тон. Вот это оформление блокнота хорошее:

А вот про такое оформление блокнота я бы сказал, что оно "не самое удачное":

Кроме того, рекомендуется оформлять описание в текстовых ячейка�� для важных ячеек с кодом.
В конце каждой строки с кодом нужно ставить символ ";" если не требуется выводить результат выполнения строки. Если нужно напечатать результат в выходную ячейку - то ";" не ставится:

Собственные функции и переменные в блокноте
При создании переменных и функций (а вместе будем называть их имена) в интерактивном режиме рекомендуется придерживаться следующих правил:
Все имена - полные слова на английском
Имена состоят из символов латиницы
Первая буква всегда строчная, т.е. в camelCase, таким образом минимальный шанс создать функцию и переменную, имя которой совпадет со встроенной
Как и системные функции - собственные функции обозначают действие
Собственные переменные - существительное
Не использовать абстрактные, однобуквенные имена или буква + цифры, за исключением общепринятых обозначений итераторов: i, k, j, m...
А теперь несколько примеров таких функций:
addOne[x_] := x + 1 (*прибавляет единицу*) getPhoneNumber[text_] := StringCases[text, phonePattern[]] createWebhook[address_] := {(*тут код*)} (*создает веб-хук*)
и переменных:
phones = {"+79991234568", "89007654321"} (*список телефонов*) currencyData = <|"ISO" -> "RUB", "Rate" -> 1, "Data" -> "2024.01.01"|>
Использование итератора:
i = 10; While[i > 0, i = i - 1; Print["current i = ", i]];
Аргументы функций и локальные переменные
Для имен аргументов функций и локальных переменных применимы следующие правила:
Они могут быть записаны либо в том виде, в каком это принято для глобальных переменных, то есть в camelCase полными словами
Часто именно для них можно использовать абстрактные имена, когда это математическая функция
Либо можно использовать сокращения
Например, для математической функции можно использовать типичные общепринятые имена аргументов вроде {n, x, y, t} и другие если у переменной нет конкретного смысла:
getNextPrimeYear[n_] := NextPrime[DateList[][[1]], n] (*n - обычно обозначает целый аргумент*) gravityForce[r_, m1_, m2_] := g * m1 * m2 / r^2 (*для функций, которые применимы в физике и других науках это тоже подходит*)
Локальные переменные и константы создаются внутри таких функций как Module, Block и With:
Module[{db = bot["Database"]}, createTable[db, "users", {"id", "name", "phone"}] ] With[{usersTable = bot["Database", "users"]}, Append[usersTable, <|"name" -> "Kirill", "phone" -> "+79991234568"|>] ]
Опции функций
У любой функции в WL могут быть необязательные аргументы в виде опций. Ниже код такой функции:
Options[sendMessage] = { "ParseMode" -> "html" }; sendMessage[bot_, chat_, message_, OptionsPattern[]] := botAPI["sendMessage", { "api_token" -> bot["Token"], "chat_id" -> chat, "message" -> message, "parse_mode" -> OptionValue["ParseMode"] }]; sendMessage[bot, 1234, "hello", ParseMode -> "markdown"]
И для опций справедливы следующие правила:
Опции объявляются до создания определения функции
Имена опций принято задавать в виде строк
Имена должны быть в PascalCase и как выше - полные английские слова
При вызове функции, опцию можно указывать как в виде строки, так и в виде символа. Изначально объявление имен опций рекомендуется делать именно как строку, так как при создании функций с опциональными аргументами внутри пакетов их не потребуется объявлять по отдельности и не произойдет перекрытия.
Сообщения
Для любого символа в языке Wolfram можно создать дополнительное сообщение. Обычно это делается в двух случаях:
Создание описания функции для авто-дополнения
Создания сообщений об ошибках
Ниже пример функции с двумя сообщениями:
createChat::usage = "createChat[chatName, user] creates chat for user."; createChat::nousr = "User `1` not exists."; createChat[chatName_, user_?userExistsQ] := callAPI["createChat", { "user" -> user, "chat_name" -> chatName }]; createChat[chatName_, invalidUser_] := Message[createChat::nousr, invalidUser];
usage - это принятый стандарт для создания описания функций и символов. А сообщение nouser содержит текст шаблона для ошибки на случай, если пользователя не существует.
Для имен сообщений справедливы следующие правила:
Имена сообщений пишутся в lowercase
Длинные имена сокращаются удалением гласных
Рекомендуется использовать от 4 до 8 символов на имя сообщения
Пакеты
В блокнотах все довольно просто, правил мало и они очень гибкие. При разработке пакетов правил, которых рекомендуется придерживаться немного больше, а их нарушение ведет к более серьезным проблемам.
Во-первых, что же такое пакет? Это файл с исходным кодом на Wolfram Language. Как выше я привел аналогию между блокнотами Mathematica и Jupyter, так и здесь проще всего сказать, что пакет WL это аналог пакета для Python. Тем более, структура файлов для них наиболее похожа.
Пакетом можно считать практически любой текстовый файл с кодом на WL, но здесь мы говорим о рекомендуемом стиле. Поэтому структура файлов для типичного пакета следующая:
[MyPackage] - [Documentation] - - [English] - - - .. - [Kernel] - - MyPackage.wl - PacletInfo.wl
[MyPackage]- директория пакета[Documentation]/[English]- директория с документацией[Kernel]- директория с исходным кодомMyPackage.wl- файл с исходным кодомPacletInfo.wl- файл с метаинформацией о пакете
Выше представлена минимальная структура файлов для пакета написанного по текущим стандартам, который предложен в Wolfram Research. Нас в первую очередь интересует файл с метаинформацией. Его содержание обычно выглядит вот так:
PacletObject[ <| "Name" -> "KirillBelov/MyPackage", "Description" -> "My package", "Creator" -> "Kirill Belov", "Version" -> "1.0.0", "WolframVersion" -> "13.3+", "PublisherID" -> "KirillBelov", "License" -> "MIT", "PrimaryContext" -> "KirillBelov`MyPackage`", "Extensions" -> { { "Kernel", "Root" -> "Kernel", "Context" -> {"KirillBelov`MyPackage`"} }, { "Documentation", "Root" -> "Documentation", "Language" -> "English" } } |> ]
Это очень похоже на package.json для пакетов npm, только вместо JSON в файле записано выражение на WL. Большинство ключей и их значения в примере выше интуитивно понятны. Контекстом называют пространство имен для функций пакета. И я должен пояснить почему имя пакета и контекст имеют такой вид. Дело в том, что текущие рекомендации по разработке пакетов (их еще называют паклетами) на языке Wolfram специально были адаптированы для Wolfram Language Paclet Repository. И по правилам теперь имя пакета и контекст должны в качестве префикса содержать имя разработчика чтобы не возникало коллизии. Это может быть логин содержащий реальное имя (как у меня), псевдоним или название компании. Чаще всего пользователи используют свои имена или имя пользователя на GitHub. Теперь перейдем к правилам создания файлов с исходным кодом.
Одиночный файл с кодом
Все пакеты обязаны содержать внутри себя только набор функций для использования скриптами или в интерактивной сессии. Пакеты не должны выполнять действия сами по себе. Они должны только создавать определения. Поэтому типичный пакет представляет собой набор публичных функций и их реализацию в приватном контексте.
Допустим я создаю пакет как в разделе выше. Мой пакет называется MyPackage. Мой ID на Paclet Repository - это KirillBelov. Поэтому контекст (пространство имен) будет KirillBelov`MyPackage`, т.е. составлено из моего ID и названия пакета. Тогда файл с исходным кодом должен располагаться в папке Kernel и имя файла совпадает с именем пакета. Именно в этом файле должен быть объявлен контекст пакета следующим образом:
BeginPackage["KirillBelov`MyPackage`"]; Begin["`Private"]; End[]; EndPackage[];
BeginPackage- объявляет новый пакет с указанным контекстомBegin- объявляет новый внутренний контекстEnd- закрывает внутренний контекстEndPackage- закрывает пакет
Между строками BeginPackage и Begin (публичный контекст) необходимо размещать объявление публичных функций пакета. По рекомендациям WRI это делается при помощи создания сообщения usage вот так:
BeginPackage["KirillBelov`MyPackage`"]; MPAddOne::usage = "MPAddOne[x] adds one to x and return result."; Begin["`Private"];
Далее между Begin и End (приватный контекст) должна быть реализация этой функции:
Begin["`Private"]; MPAddOne[x_] := x + 1; End[];
В принципе минимальный пакет готов. Если он установлен в систему, то его можно загрузить в сессию вот так:
<<KirillBelov`MyPackage`
А затем использовать публичную функцию вот так:
MPAddOne[2] (* => 3 *)
При этом в интерфейсе Mathematica (или в WLJS) при вводе появится вот такая подсказка:

Оформление кода пакета
Ну а теперь собственно к правилам оформления кода.
Между выражениями и определениями в пакете необходимо ставить две пустые строки. Как в примерах кода выше. Две пустые строки Mathematica автоматически распознает как границу между ячейками и отображает код пакета в наиболее удобном виде:

Один и тот же код в VS Code и в Mathematica В конце каждой строки рекомендуется ставить символ
";", так как в Mathematica пробел может интерпретироваться как умножение. Часто (даже в исходниках Mathematica) я встречаю ошибки с количеством скобок, которые не приводят к падению загрузки пакета, а просто умножают две несвязанные строки.
Имена публичных функций пакета
Все собственные функции пакета из публичного контекста должны быть:
В PascalCase в виде осмысленных английских слов/фраз/предложений и обозначать действие, т.е. в точности, как встроенные функции
Но в отличии от встроенных функций, для функций пакетов необходимо указывать префикс, который указывает на имя пакета, в котором определена функция
Префикс может быть очевидным сокращением или полным названием пакета. Например, в пакете
OpenAILink`все функции начинаются сOpenAI*. А функции загруженные из пакетаNeuralNetworks`начинаются с префиксаNet*.
Следуя этим правилам я назвал функцию из демо-пакета как MPAddOne. А также добавил описание для этой функции в виде сообщения usage. Сообщение составляется по следующему шаблону:
MyFunc::usage = "MyFunc[args] what do MyFunc.";
То есть в начале строки описания нужно обязательно написать функцию + аргументы, а дальше описание того, что функция делает. В конце ставится точка. Кроме того можно создать описание для каждой перегрузки функции:
MyFunc::usage = "MyFunc[x] add 1 to x. MyFunc[x, y] add y to x.";
Тогда меню автодополнения будет отображать все перегрузки функции:

Публичные константы и символы пакета
Часто бывает так, что необходимо создать не только набор функций, но и хранить некоторые данные пакета. Для этих целей используют символы с отложенным вычислением или константы. Все они записываются как системные константы, но внутри пакета. Их так же необходимо объявлять с помощью usage, но без шаблона вызова. Хороший пример такого пакетного символа - это ключ $OpenAIKey из пакета OpenAILink`. Для демо-пакета я создам константу, которая хранит текущую директорию установки. Это распространенная практика и она бывает очень полезна, если вместе с паклетом в систему устанавливается папка с примерами данных, которые пользователю нужно показать:
$MyPackageDirectory::usage = "My package directory."; Begin["`Private`"]; $MyPackageDirectory = DirectoryName[$InputFileName];
Т.е. публичные константы пакета:
Объявляются с
usageСодержат префикс из $ + имя пакета или сокращение
Записаны в PascalCase
Хранят данные связанные с самим пакетом
Приватные имена пакета
В приватном контексте кроме определения публичных функций могут храниться приватные функции. Они не доступны пользователю и тем самым не создают хаос и садомию, если в двух пакетах будут одинаковые приватные имена. Имена приватных функций должны быть такими же как и имена собстве��ных функций в интерактивной сессии, т.е.:
camelCase из полных слов английского языка
Функции обозначают действия
Префиксы не требуются, так как приватные функции видны только в приватном контексте
Описание в виде
usageне требуетсяВсе тоже самое касается приватных переменных пакета и констант
Ниже пример такой функции:
Begin["`Private`"]; MyFunc[x_] := myPrivateFunc[x, 1]; myPrivateFunc[x_, y_] := x + y; End[];
Несколько исходных файлов
Если файлов с исходным кодом несколько, то для каждого файла должен быть собственный контекст, где последняя часть совпадает с именем файла. Например структура файлов:
[MyPackage] - [Documentation]/.. - [Kernel] - - MyPackage.wl - - API.wl - - DB.wl - PacletInfo.wl
Обязателен файл MyPackage.wl, который в данном случае стоит называть инициализирующим
API.wl и DB.wl содержат специфичные связанные функции
PacletInfo.wl необходимо дополнить, чтобы он новую отражал структуру
Во-первых, как нужно изменить PacletInfo.wl чтобы он поддерживал такую структуру паклета? Необходимо в раздел "Extensions"/"Kernel"/"Context" добавить все файлы и соответствующие им контексты вот так:
"Extensions" -> { { "Kernel", "Root" -> "Kernel", "Context" -> { { "KirillBelov`MyPackage`", "MyPackage.wl" }, { "KirillBelov`MyPackage`API`", "API.wl" }, { "KirillBelov`MyPackage`DB`", "DB.wl" } } } }
Тогда при загрузке пакета в сессию WL поймет в каком файле какой контекст и будет правильно их находить. Еще одна важная функция такого объявления файлов и контекстов в том, что инструменты сборки и публикации пакетов используют эту информацию для правильного создания архива.
Поделив исходный код на несколько файлов - мы должны создать специальный инициализирующий файл, имя которого совпадает с именем паклета. Теперь MyPackage.wl должен содержать вот такой код:
BeginPackage["KirillBelov`MyPackage`"]; EndPackage[]; Get["KirillBelov`MyPackage`API`"]; Get["KirillBelov`MyPackage`DB`"];
Код выше сначала создает основной контекст без определений, а затем загружает дочерние контексты. Поиск дочерних контекстов (ведь имена файлов напрямую не указаны) происходит при помощи мета-информации в PacletInfo.wl. А вот такой код будет в API.wl:
(* ::Package:: *) BeginPackage["KirillBelov`MyPackage`API`"]; $MyPackageAPIVersion::usage = "current version of the API."; MyPackageGetCurrencyRates::usage = "MyPackageGetCurrencyRates[] returns current rates."; Begin["`Private`"]; $MyPackageAPIVersion = "1.0.0"; MyPackageGetCurrencyRates[] := { <|"ISO" -> "RUB", "Rate" -> 1.0|>, <|"ISO" -> "USD", "Rate" -> 30.0|> }; End[]; EndPackage[];
Допустим, дочерний пакет DB.wl должен использовать функции из API.wl. Это необходимо указывать в виде списка используемых контекстов при создании контекста вторым аргументом функции BeginPackage:
(* ::Package:: *) BeginPackage["KirillBelov`MyPackage`DB`", { "KirillBelov`MyPackage`API`" }]; MyPackageDBSaveCurrency::usage = "MyPackageDBSaveCurrency[currency] saves currency to database. MyPackageDBSaveCurrency[] saves all currencies to database"; Begin["`Private`"]; MyPackageDBSaveCurrency[currency_] := With[{db = getDBInstance[]}, AppendTo[db, currency]; ]; MyPackageDBSaveCurrency[] := Map[MyPackageDBSaveCurrency] @ MyPackageGetCurrencyRates[]; End[]; EndPackage[];
Таким образом мы можем удобно организовать структуру достаточно большого проекта и настроить зависимости между контекстами и функциями пакета. В качестве примера я приведу структуру паклета TelegramBot, состоящую из нескольких файлов:

Скобочки, переносы, пробелы и сокращения
В Wolfram Language есть сразу несколько видов скобочек и огромное количество синтаксического сахара. строгих требований нет, но я опишу тот формат, который на мой взгляд наиболее удобен:
Скобочки, функции и локализующие конструкции в java-style
Пробелы слева и справа от операторов:
{+, -, *, /, ^, &&, ||, >=, <=, ==, ===, >, <}Пробелы слева и справа от присваиваний и правил:
{=, :=, ->, :>, /:, /;, /., /*, ...}Пробелы после запятых, двоеточия и точки с запятой
Отдельно-стоящие списки и ассоциации в C#-style
Строки кода более 120 символов нежелательны
Табуляция в 4 проблема. По умолчанию Mathematica использует символ табуляции
Не злоупотребляйте псевдонимами и сокращениями
Собственно несколько примеров:
myFunc1[x_, y_] := Module[{z = x + y}, z + z ^ 2 - z / 2 * 3 ] (*операторы, локализующая функция и присваивание*) (*правила, замена, условный оператор и условие*) {1, 2, 5, 4, 2, 0} //. {f___, x_, y_, r___} :> {f, y, x, r} /; y < x (*список рядом с "=" в java-style и отдельная ассоциация в C#-style*) users = { <| "Name" -> "Kirill", "Language" -> "WL" |> }
И отдельно я бы хотел сказать про антипаттерн со злоупотреблением синтаксическим сахаром. Это в первую очередь относится к исходному коду, но прощается в блокнотах, так как весь синтаксический сахар был придуман именно для интерактивного режима. Я имею ввиду использование чудовищных конструкций вроде этой:

Определения функций
И последний раздел о том, как рекомендуется создавать определения. Эти рекомендации помогут правильно организовать самую важную составляющую любого проекта на WL - собственные функции (а как мы выяснили типичный проект на WL - это набор функций, а затем их вызов):
Рекомендуется указывать типы параметров или накладывать на них условия
Не использовать глобальные переменные
Использовать локальные переменные и
ModuleВсегда возвращать результат
Желательно указывать в комментариях тип результата
Накладывать условия и печатать сообщения при неправильном вызове
Не менять состояние окружения, а только преобразовывать данные, если этого не требует бизнес-логика
А теперь я поясню отдельно каждый пункт.
Указание типов. Это делается очень легко и очень помогает быстро понимать что делает функция. Можно сравнить код двух функций ниже и польза будет очевидна:
selectDates[data_] := Select[data, DateObjectQ]; selectDates[data_Association] := Select[data, DataObjectQ];
В первом случае мы не знаем объект какого типа передается на вход, а во втором мы знаем что это ассоциация, а значит можем понять какие еще функции можно применить специфичные для ассоциаций и что делать с результатом дальше.
Не использовать глобальные переменные. Это очевидно плохо, так как состояние переменные всегда может измениться.
Локальные переменные и Module. Рекомендуется в общем случае использовать именно Module, так как эта функция создает новые уникальные переменные внутри и удаляет их после выполнения блока кода. Пусть имеется определение:
addSqrt[x_] := Module[{sqrt}, sqrt = x ^ 2; x + sqrt ]; addSqrt[2] (* => 6 *)
В этом определении используется переменная sqrt. Если даже в текущей сессии будет где-то использоваться глобальная переменная с этим именем, то это никак на нее не повлияет:
sqrt = 4; addSqrt[3] (* => 12 *) sqrt (* => 4 *)
Но если убрать Module, то произойдет следующее:
sqrt = 4; addSqrt[x_] := ( sqrt = x ^ 2; x + sqrt ); addSqrt[3] (* => 12 *) sqrt (* => 9 *)
Почему не рекомендуется в общем случае использовать Block? Это функция очень похожа на модуле, но она не создает в момент выполнения новой переменной. Вместо этого она очищает значение локальной переменной. Пока что разница не ясна, но я покажу ее на примере кода. Пусть есть несколько определений, которые зависят друг от друга:
(*выбирает максимальную точку из двух по модулю*) maxPoint[{p1_List, p2_List}] := If[Total[p1^2] > Total[p2^2], p1, p2]; (*получает значение компоненты X максимальной точки*) getMaxX[{p1_List, p2_List}] := maxPoint[{p1, p2}][[1]];
А теперь я создам третьею функцию забыв о том, что вторая вызывает первую. И внутри использую локальную переменную maxPoint:
moveMaxX[{p1_, p2_}] := Module[{maxPoint}, getMaxX[{p1, p2}] + 1 ]; moveMaxX[{{1, 2}, {3, 4}}] (* => 3 + 1 == 4 *) moveMaxX[{p1_, p2_}] := Block[{maxPoint}, getMaxX[{p1, p2}] + 1 ]; moveMaxX[{{1, 2}, {3, 4}}] (* => {{2, 3}, {4, 5}} *)

Что произошло на скриншоте выше? В первом случае с Module все сработало правильно, а во втором с Block определение maxPoint было очищено на время вызова функции, а значит оно не сработало когда функцию maxPoint пыталась вызвать функция getMaxX. По сути определение maxPoint перестало существовать внутри Block для всех других функций. И в итоге результат получился непредсказуемым. Поэтому функцию Block рекомендуется использовать только если пользователь точно знает, что требуется сделать, а во всех остальных случаях использовать Module.
С With ситуация еще сложнее. По сути в нем создаются не переменные а константы. Продемонстрировать это можно вот так:
Module[{arr = {1, 2, 3}}, AppendTo[arr, 4] ] (* => {1, 2, 3, 4} *) With[{arr = {1, 2, 3}}, AppendTo[arr, 4] ] (* => error *)
Во втором случае arr сам по себе является неизменяемым списком, а значит на нем нельзя вызывать функцию AppendTo и вообще работать как с переменной. Нужно просто учитывать, что при использовании With на место arr подставится {1, 2, 3} именно в таком виде.
Возвращение результата. Wolfram Language оперирует неизменяемыми выражениями. А значит в нем не принято (хотя технически можно) вызывать функции, которые ничего не возвращают, но меняют состояние окружения. Все вызовы функций должны принимать на вход аргументы и возвращать преобразованный результат.
Указание типа результат. Это опциональная рекомендация. Я часто этим пользуюсь, чтобы сделать код более читаемым. Вот пример моего кода, где я это активно использовал:

Сообщения. Это очень важный пункт, про который многие забывают. Либо его просто игнорируют, так как он не влияет напрямую на функциональность. Но сообщения об ошибках помогают в отладке и рефакторинге кода. При создании определения рекомендуется создать дополнительную перегрузку которая вернет Null и напечатает сообщение с ошибкой и некорректными аргументами. Это делается вот так:
(*само сообщение*) myFunc::argx = "`1` called with `2` arguments. `3` arguments is expected."; (*ошибка аргументов*) myFunc[args___] := Message[myFunc::argx, myFunc, Length[{args}], 2]; (*основное определение*) myFunc[x_, y_] := x + y; (*вызов функции*) myFunc[1, 2] myFunc[3]

Не менять состояние окружения. Это простое правило связано с тем, что все выражения в WL неизменяемы. Изменить можно только символы и их значения, но не сами выражения. Поэтому именно внутри определений функций не рекомендуется мутировать состояние среды, хотя никто не запрещает мутировать переменные, которые были переданы в функцию в качестве аргумента. В этом случае, если бизнес-логика требует изменения переменных правильнее всего создавать функции специальным образом с использованием удерживания. Допустим есть таблица с пользователями и функция должна добавлять пользователя в нее:
users = { <|"Username" -> "Kirill", "Language" -> "WL"|>, <|"Username" -> "Eugeny", "Language" -> "C#"|> }; addUser[table_List, user_Association] := ( AppendTo[table, user]; table = DeleteDuplicatesBy[table, #Username &]; table ); addUser[users, <|"Username" -> "Ivan", "Language" -> "Russian"|>]

В примере выше ошибка заключается в том, что при вызове функции на место users сразу подставляется неизменяемое выражение, к которому нельзя ничего добавить. Функции AppendTo и Set работают только на символах, но как этого добиться? Нужно не дать вычислиться переменной users! Делается это при помощи добавления атрибута HoldFirst, который удерживает аргумент от вычисления:
users = { <|"Username" -> "Kirill", "Language" -> "WL"|>, <|"Username" -> "Eugeny", "Language" -> "C#"|> }; SetAttriutes[addUser, HoldFirst]; addUser[table_?ListQ, user_Association] := ( AppendTo[table, user]; table = DeleteDuplicatesBy[table, #Username &]; table ); addUser[users, <|"Username" -> "Ivan", "Language" -> "Russian"|>]

users изменилсяТаким образом, рекомендуется для всех функций указывать Hold* атрибуты, которые согласно бизнес-логике должны изменять существующие переменные.
Заключение
Соблюдаю ли я сам все эти правила? Конечно же нет. Очень часто я использую то, что позволяет язык, но я не встречал этого ни в одном проекте и репозитории. Я очень сильно страдаю когда смотрю на свой же код, который идет в разрез описанному выше стилю и пытаюсь его исправить - сделать более читабельным. Иногда получается, а иногда нет. Когда я только начинал изучать WL меня просто поразило многообразие и сложность его синтаксиса (а точнее синтаксического сахара). Я старался использовать его везде где только мог в самых невообразимо-сложных формах. Затем, когда я возвращался к своему же коду - то был просто в ужасе! Теперь только Господь знал, что делает мой собственный код. И вот спустя годы использования WL я постарался отбросить все сложности, писать код как можно проще и сохранить свои текущие представления о красивом коде здесь.
Всем спасибо за внимание!
