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