Роберт знает толк в стиле
Роберт знает толк в стиле

Я как большой фанат 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). В интерактивных блокнотах можно создавать ячейки с кодом, текстом и форматированием, выполнять ячейки с кодом и тут же видеть результат. Поэтому в интерактивных блокнотах могут быть:

  • Ячейки с длинными последовательностями команд, которые можно назвать скриптами

  • Создание переменных

  • Создание собственных функций

  • Вызов встроенных и собственных функций

Рекомендуется всегда придерживаться одного очень простого правила - одно логическое действие - одна ячейка, то есть писать множество функций, тут же их вызывать, создавать переменные и что-то рисовать в одной ячейке - это плохой тон. Вот это оформление блокнота хорошее:

Модель SIR
Модель SIR

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

Все в одной ячейки без возможности отладки по отдельности
Все в одной ячейки без возможности отладки по отдельности

Кроме того, рекомендуется оформлять описание в текстовых ячейках для важных ячеек с кодом.

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

График функции y = sin(x) по точкам
График функции y = sin(x) по точкам

Собственные функции и переменные в блокноте

При создании переменных и функций (а вместе будем называть их имена) в интерактивном режиме рекомендуется придерживаться следующих правил:

  • Все имена - полные слова на английском

  • Имена состоят из символов латиницы

  • Первая буква всегда строчная, т.е. в 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
    Один и тот же код в 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 изменился
Теперь users изменился

Таким образом, рекомендуется для всех функций указывать Hold* атрибуты, которые согласно бизнес-логике должны изменять существующие переменные.

Заключение

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

Всем спасибо за внимание!

Комментарии (23)


  1. JerryI
    16.01.2024 10:43
    +1

    Я думаю еще один возможный паттерн стоит упоминания.


    Это в духе ES, Python и их импортов. Скажем, есть файл - "библиотека", но не хочется ее оформлять как настоящий Paclet, а что-то по компактнее и локаничнее

    Module.wl

    BeginPackage["ContextName`", {"Другие контексты, которые нужны"}]
    
    SomeVariable = 777;
    SomeFunction[_] := RandomInteger[{0,10}];
    
    EndPackage[]
    
    {ContextName`SomeFunction, ContextName`SomeVariable}


    Тогда где-то в вашем основном файле или блокноте программы - делаем один раз

    {func, var} = Get["Module.wl"];
    
    func[123]


    1. KirillBelovTest Автор
      16.01.2024 10:43
      +1

      В итоге в сессии будет доступно сразу:

      func
      var
      someVariable
      someFunction
      

      В таком случае все таки лучше оформлять пакет как скрипт, где определения идут подряд. Но беда такого способа в том, что любая локальная переменная будет засорять текущий контекст пользователя.


      1. JerryI
        16.01.2024 10:43
        +1

        Да, верно. Но все равно в вашей программе найдется где-то главный Main, где нужны будут обычные переменные. Впрочем, ниче не стоит все еще раз завернуть в BeginPackage :)


      1. JerryI
        16.01.2024 10:43
        +1

        Окей, понял свою ошибку. Извиняюсь, вот исправленная версия

        Module.wl

        BeginPackage["ContextName`"]
        
        Begin["`DontLookAtMe`"]
        
        SomeVariable = 777;
        SomeFunction[_] := RandomInteger[{0,10}];
        
        End[] 
        
        EndPackage[]
        
        {ContextName`DontLookAtMe`SomeFunction, ContextName`DontLookAtMe`SomeVariable}


        1. KirillBelovTest Автор
          16.01.2024 10:43

          Да, это сработает, но противоречит правилам, которые предлагаются в WRI. Без usage примерно тоже самое, но ближе к конвенции это вот так (подразумеваются двойные переносы):

          BeginPackage["ContextName`"];
          
          SomeVariable;
          
          SomeFunction;
          
          Begin["`DontLookAtMe`"];
          
          SomeVariable = 777;
          
          SomeFunction[_] := RandomInteger[{0,10}];
          
          End[]; 
          
          EndPackage[];
          


  1. Shilov_N_Jr
    16.01.2024 10:43
    +2

    Очень хорошая статья! Два замечания (не в укор, а именно от слова "заметить"):

    1. К "использованию итератора" можно было добавить комментарий вроде "но обычно в WL можно обойтись без них" или даже "но чаще всего в WL их использование неэффективно".

    2. Можно было добавить немного рассуждения о пакетах: почему их создание требует столько сил? Оправдано ли это, если код пишется для личного пользования? Раз уж вспомнили Python, то в нём мне достаточно простенького init.py-файла, никакую структуру задавать не надо, и всё работает. Разумеется, я не выходил в продакшн, нигде не деплоил, ... (больше программистских слов не знаю). Но и в WL я ведь, вероятно, не на широкого пользователя работаю, а только для своего удобства модульную структуру создаю!


    1. JerryI
      16.01.2024 10:43
      +1

      Добавлю свои две копейки к пункту 2. :)

      Поддерживаю, ведь не обязательно в принципе специфичного для проекта мелкого модуля создавать полноценную библиотеку. Я бы в этих случаях просто обозначал BeginPackage[], для того, чтобы просто изолировать его переменные, затем, сохранял бы его в директорию проекта и потом заполучал Get-ом.

      Но мне этим и нравится WL, что можно так, можно эдак, можно вовсе засунуть весь код в Module и также Get-ом его получить.


    1. KirillBelovTest Автор
      16.01.2024 10:43

      Сначала отвечу на первый пункт.
      Во-первых, как говорят Джейсон Стэтхэм и тимлид нашей команды: "Лучше меньше кода, чем больше кода. 400 строк - это хороший мердж-реквест, 20 строк - отличный, а если удалил 100 строк, то просто великолепный". Привычный глаз легко разбирает все псевдонимы и сокращения на WL, но их такое огромное разнообразие, что я, например, постепенно от них отказываюсь и оставляю только самые лаконичные и очевидные. Например, вот это на мой взгляд хорошо:

      Map[func] @ {1, 2, 3, 4}
      

      А вот это уже значительно хуже, так как тут явное злоупотребление для одной строки, хоть и совсем небольшое:

      Exp[#^2 - Sin[#] + Log[x^1/2]& /@ {1, 2, ,3 , 4}
      

      И это еще очень короткий пример, я встречал огромные выражения, в которые надо вглядываться по полчаса чтобы понять что там происходит. Обычно их мне отправлял Пётр, а я просто говорил, что не понимаю, что там написано. Поэтому возможность отказаться от итератора в пользу функций высшего порядка + чистые функции не всегда приводит к более читаемому коду.

      Во-вторых, это какой-то стереотип на счет итераторов и того что они являются плохим тоном в WL. Да, For очень неудобен, но While - это самый удобный способ создать бесконечный цикл, а Table самая лучшая, читаемая и очевидная функция для обработки списков. Даже не смотря на то, что я часто пользуюсь функциями высшего порядка - Table для меня не является чем-то плохим. К стандартным циклам вроде For такое отношение из-за их громоздкости и того, что они не локализуют итератор. И кстати говоря в Table вполне работает автокомпиляция как и в Map.


    1. KirillBelovTest Автор
      16.01.2024 10:43

      И по второму пункту. Как я и писал в статье - любой текстовый файл с кодом на WL можно назвать пакетом, но в статье я описал правила и рекомендации по разработке стандартизированных пакетов. Естественно, если вам не требуется пакет соответствующий "стандарту" вы можете просто создать один файл с расширением .wl, перечислить в нем определения, затем добавить папку с ним в $Path и использовать без всех тех ритуалов что я описал. Т.е. например файл MyPack.wl в директории ~/MyPack:

      func1[x_, y_] := x + y; 
      

      Код добавления пакета в пути поиска (один раз в $UserBaseDirectory/Kernel/init.m):

      $Path = DeleteDublicates[Append[$Path, "~/MyPack"]]; 
      

      И в блокноте:

      <<MyPack`
      
      func1[1, 2] (* => 3 *)
      

      В итоге папка MyPack всего с одним файлов уже будет готовым пакетом, просто не по стандартам WRI.

      И второе, что я хотел сказать по этому пункту. Дело в том, что между WL и python есть различие в структуре кода и файлов. Что касается импорта пакетов, то python обрабатывает файловую структуру, а сам код уже на втором месте. То есть создав простой модуль его имя и способ загрузки зависят от файла и его расположения, но не от кода внутри. А в WL в первую очередь важен сам загружаемый код и неважно где он находится. Создать и загрузить в сессию полноценный пакет можно прямо в самой сессии, для этого даже не нужно создавать никакие файлы. А вот PacletManager как раз стандартизирует подход к организации паклетов именно через структуру файлов и папок, то есть смещает фокус в сторону, которая ближе к Python.


  1. fujinon
    16.01.2024 10:43
    +3

    Приятно видеть статьи по Mathematica. А что за IDE на картинках и платное ли?


    1. KirillBelovTest Автор
      16.01.2024 10:43
      +4

      Спасибо! Кроме самой Mathematica я использую VS Code вместе с официальный плагином для Wolfram Language от Wolfram Research. Он бесплатный. Еще если его правильно настроить (указать путь до ядра), то он начинает работать значительно лучше - показывает usage и лучше работает автодополнение.

      Кроме этого я упоминал про бесплатный интерфейс WLJS Notebook для Wolfram Engine который разрабатывает @JerryI и я.

      А еще у меня есть старая статья с обзором бесплатных инструментов для WL. Сейчас часть информации оттуда устарела, может быть в будущем я напишу продолжение. Если у вас есть еще вопросы - с радостью отвечу =)


    1. KirillBelovTest Автор
      16.01.2024 10:43

      Я ж совсем забыл. На хабре тоже есть две статьи про WLJS Notebook


  1. iggr63
    16.01.2024 10:43
    +3

    Очень расширенное описания. Блокноты, ячейки, сессии очень похожи на jupyter notebook, но конечно Wolfram сделал это гораздо раньше.


  1. Refridgerator
    16.01.2024 10:43
    +3

    я изучил много встроенных пакетов в Mathematica/Wolfram Language, которые были написаны разработчиками из Wolfram Research. В большинстве случаев они были написаны еще хуже

    А всё потому что:

    а) они написаны математиками с бэкграундом на фортране и

    б) сам язык к этому провоцирует.

    А) значит, что программировать надо учиться. Не просто выучить синтаксис и стандартную библиотеку - а как строить архитектуру, разбивать задачи на подзадачи и всё такое. Нужно иметь примеры хорошо написанного кода, коих в других языках навалом (не считая фортрана, конечно).

    Б) значит, что WL изначально не задумывался как мультипарадигменный язык программирования. Он задумывался для решения конкретной задачи (символьный калькулятор), которую делал хорошо и которую до сих пор делает лучше конкурентов. Но всё остальное - это костыли, которые логически из постановки решаемой задачи вытекают слабо. Например - это функции Reap и Sow, необходимые для пополнения одного списка в процессе итерирования другого без оверхеда. Ну а когда выбирать SetDelayed, а когда RuleDelayed - это отдельное интересное приключение. И кстати,

    А теперь несколько примеров таких функций:

    addOne[x_] := x + 1

    Это не функция, а правило замены. Функция определяется как

    addOne = Function[{x}, x + 1]

    Разница прочувствуется, когда в аргументе для правила замены передать другое правило замены, у которых имена заменяемых переменных пересекаются и всё это в Plot с Manipulate засунуть.

    я встречал огромные выражения, в которые надо вглядываться по полчаса чтобы понять что там происходит. Обычно их мне отправлял Пётр, а я просто говорил, что не понимаю, что там написано

    Это и назывется "write-only" код. Ещё одна причина, по которой я не использую WL в качестве ЯП общего назначения. А если мой код сломается - то даже господь не будет знать, почему.

    Справедливости ради, есть язык ещё хуже - это Matlab. Каждый раз, когда приходится с ним пересекаться, один и тот же вопрос возникает - что курили разработчики, когда придумывали его синтаксис и названия для функций? Наверно поэтому в нём овермиллион тулбоксов на все случаи жизни, чтобы избежать программирования любой ценой.


    1. KirillBelovTest Автор
      16.01.2024 10:43
      +1

      Это не функция, а правило замены

      addOne[x_] := x + 1

      Не думаю, что вам нужно что-то доказывать, так как я помню, что вы хорошо знаете WL и читали предыдущие статье где я писал про правила и шаблоны. Но я должен поправить терминологию. Выше - это функция. Функция в WL - это символ, который имеет DownValues или SubValues (но вообще можно и с OwnValues создать "функцию", т.е. символ, который применяется к набору аргументов с квадратными скобками). Она только использует правило замены в определении, но сама по себе не является правилом замены. Правила замены это однозначно только:

      rules = {
          x -> 10, 
          time :> Now
      }; 
      

      Функция определяется как

      addOne = Function[{x}, x + 1]

      А это принято называть "чистая функция". Если использовать вашу терминологию, то в итоге выражение Function[..] тоже сведется к правилу замены. Ведь абсолютно все определения символов в WL хранятся в списках определений в виде правил замены, примерно как в анекдоте про урок физики в церковной школе... ????


      1. Refridgerator
        16.01.2024 10:43
        +1

        Не-не, на хорошее знание WL я не претендую никак, а уж тем более по сравнению с вами) Просто если говорить о хороших практиках, то определение Function в явном виде будет более предпочтительным, и узнал я об через личную боль и страдания. А во что это определение превратится в недрах языка - мне, как пользователю, знать должно быть необязательно.


        1. KirillBelovTest Автор
          16.01.2024 10:43
          +3

          Чистая функция Function хороша по своему, но имеет недостатки на фоне функций (их еще иногда называть шаблонными функциями). Главный недостаток - сложность и громоздкость работы собственно с шаблонами и громоздкость создания переопределений и перегрузок. А некоторые вещи для чистой функции вообще недоступны, так как по сути это неизменяемое выражение, а обычная (шаблонная) функция - это символ с определением.

          В чем преимущество чистых функций:

          1. Авто-компиляция при использовании внутри функций высшего порядка

          2. Более простой и стандартный вид - в них невозможно использовать громоздкие шаблоны

          3. Возможность использовать как лямбда-функцию не создавая символов внутри сессии тем самым не нагружая сборщик мусора и не засоряя память

          Авто-компиляция. Магия, которая срабатывает если в одном месте и в одно время встретились:

          • Чистая функция

          • Функция высшего порядка

          • Упакованный массив

          func = #^2 + 1&; (*чистая функция*)
          mapFunc = Map[func]; (*функция высшего порядка + чистая функция*)
          packedArray = Range[10000]; (*упакованный массив*)
          Developer`PackedArrayQ[packedArray] (*так можно убедиться что он упакован*)
          
          mapFunc[packedArray] (*тут сработает автокомпиляция, т.е. код выполнится быстрее*)
          

          Если одной из трех составляющих нет, то автокомпиляция не сработает. Т.е. например в цикле каждый шаг будет отправлять в байткод, а потом в машинный код и на процессор по отдельности, а не весь цикл за раз.

          Более стандартный вид. Ну тут очевидно, что упрощение и более строгие рамки ведут к более понятному коду.

          Использование как лямбда-функции. Собственно это в первом примере и показано. Но я должен объяснить один момент про символы. Дело в том, что каждой символ - это отдельный объект в ядре. При его создании происходит выделение памяти в процессе и куча еще какого-то оверхеда связанного с выделением места в списке определений. Поэтому без особой нужды символы лучше не создавать. А если нужна функция и она достаточно простая и короткая - то можно обойтись чистой функцией.

          Я хочу сказать, что не считаю, что тема выбора между чистой и шаблонной функцией относится именно к стилю написания кода. Какие создавать определения и какие функции языка использовать - решает сам программист. А вот как правильно оформить пакет или назвать функцию - это я и постарался описать с объяснением причин и рекомендациями WRI.
          (чуть позже я отвечу на другие ваши замечания в первом комментарии)


    1. KirillBelovTest Автор
      16.01.2024 10:43
      +3

      они написаны математиками с бэкграундом на фортране

      Это очень спорный вопрос. Mathematica почти в текущем виде появились в 1988 и естественно в те времена по моим представлениям было очень трудно найти хорошего математика, который знал бы все паттерны проектирования и умел проектировать архитектуру приложений как на глобальном уровне всего приложения, так и в те моменты, когда вопрос касается небольших функций. Ведь "Чистый код" впервые опубликовали в 2008, а принципы SOLID решили так назвать и начали всем рекомендовать в начале 2000-х. И вообще много чего тогда не было. Но что на мой взгляд заслуживает внимания - первые разработчики сначала SMP, а затем Mathematica сразу начали писать код на C и затем на С++. А ведь SMP была выпущена в 1981! И это приложение уже придерживалось всех принципов, которые затем перешли в Mathematica. Только представьте как сложно было найти математиков с опытом программирования на С++ с учетом того, что С++ появился в 1984.

      WL изначально не задумывался как мультипарадигменный язык программирования

      Все верно, изначально он вообще не задумывался как язык программирования, но в итоге стал им. И я не считаю, что он для этого не предназначен. Если WL нельзя использовать как язык программирования, а пригоден он только для решения задачек по математическому анализу и физике, то та же участь должна постичь такие языки как JS или С. Они ведь тоже провоцируют писать код как попало. На них точно так же совсем небольшой процент действительно хорошего кода, но эти языки все еще остаются самыми популярными в мире.

      Cам язык к этому провоцирует

      Язык очень гибкий и на мой взгляд оказался в заложниках своей истории. А история говорит нам, что Mathematica эта штука для развлечения ученых и не более. Его гибкость и принципы исполнения кода не провоцируют, а позволяют делать что угодно и как попало в любом виде. Я перед тем как опубликовать статью и предвидя комментарии касающиеся "write-only" кода спросил в группе в вк о примерах "чудовищных выражений на WL". Не так много ответили как мне хотелось бы, но вот несколько таких:

      Position[Nest [Append [#1, If [#3 > 1, #1[­[-1]]/#3, 3 #1[­[-1]] + #2 + 1]] & @@ {#1, #2, GCD[#1[­[-1]], #2]} & @@ {#, Length@# + 1} &, {1}, 10^4], 1][­[All, 1]]
      
      Image@Total[NestList[Transpose[Mod[.91 + # . BoxMatrix[1]/3, 1] & /@Transpose[i = 1; RotateLeft[#, i--] & /@ #]] &,SparseArray[{_, 51} -> 1, {3, 101}], 101], {2}]
      

      Там на стене можно и другие поискать, а еще есть tweet a program. В общем к чему я - всего одна строчка, а прочитать ее составляет огромных усилий. И это тот самый антипаттерн из статьи. Такой код хорош если его нужно выполнить один раз или когда его нужно написать в качестве упражнения, где короткое и запутанное выражение является самоцелью. Но если нужно сделать действительно полезную библиотеку или приложение, то придется себя дисциплинировать и следовать все принципам. Это в точности то, зачем я написал эту статью! Какие-то языки жестоко наказывают программиста если они что-то делают не по конвенции - я считаю, что это по своему прекрасно. Но в WL такого нет, единственный вариант чтобы код не сработал - это написать его синтаксически неверно. Тогда он просто не выполниться, а все остальное работает по принципу "ты это хотел, ты это получил" (из того великолепного перевода, что вы мне скинули выше, который мы читали всей командой).

      Мой вывод касательно этого пункта такой: до тех пор пока пользователи не будут относиться к языку серьезно, а считать сам язык плохим и провоцирующим на ужасный код - код на нем таким и останется. Легкость освоения на первоначальном этапе и сложность на последующих сыграли с WL злую шутку. Очень многие остановились на возможности построить график и взять интеграл, а большего им никогда и не потребуется (в чем нет ничего плохого). Но из-за этого большого количества хороших примеров так никогда и не появится. А еще есть преграды в виде цены, закрытых исходников и отсутствия вакансий в коммерческой разработке, которые могли бы стимулировать программистов изучать язык, писать хороший код и тем самым улучшать сам язык и привлекать других людей.

      Но всё остальное - это костыли, которые логически из постановки решаемой задачи вытекают слабо.

      Вот тут я не совсем понимаю, что вы имеете ввиду под костылями. Я помню пример с обработкой аудио, которая просто плохо реализована, но там мои полномочия все..._ В Mathematica очень много других подключаемых библиотек написанных на других языках вполне нормально работающих. Хотя вот работу с сокетами мне пришлось полностью переделать, так как встроенная реализация тоже была плохая. В общем я частично согласен, но не стал бы приравнивать функции конкретных пакетов и сам язык.

      Например - это функции Reap и Sow, необходимые для пополнения одного списка в процессе итерирования другого без оверхеда.

      Если под оверхедом вы имеете ввиду то, что список нельзя изменить, а можно только создать новый, то я б не назвал это костылем. Таков принцип языка, что все выражения неизменяемы и только значения символов можно изменить. Но это естественным образом приводит к снижению эффективности, и в итоге появились некоторые функции, которые не до конца соблюдают принцип неизменяемости. Sow/Reap используют внутри себя функцию Internal`Bag, которая позволяет добавлять значения в структуру данных за O(1) и затем возвращать список. А вот накопление в пустой список происходит за O(n) в наивном случае. Но вы легко можете создать свой накопитель без оверхеда вот так:

      SetAttributes[MyList, HoldAll]
      
      MyList[] := With[{array = Unique["MyList`array$"], length = Unique["MyList`length$"]}, 
      	array = {0}; 
      	length = 0; 
      	MyList[array, length]
      ]
      
      MyList /: Append[MyList[array_Symbol, length_Symbol], element_] := (
      	length = length + 1; 
      	If[IntegerQ[Log2[length]], array = Join[array, ConstantArray[0, length]]];
      	array[[length]] = element;
      )
      

      А вообще сейчас уже есть изменяемые структуры данных (да они появились поздно), которые создаются при помощи CreateDataStructure.

      Ну а когда выбирать SetDelayed, а когда RuleDelayed - это отдельное интересное приключение.

      Да, этот тот самый случай, когда для понимания разницы нужно чуть-чуть больше усилий, чем на то, чтобы научиться строить графики и решать уравнения. Я сам хоть и постоянно пользовался Математикой в университете, но только спустя годы понял, что это и зачем вообще нужно. Нужно чтобы тех, кто знает как и когда применять -> а когда :> было больше и чтобы они писали красивый код!

      Значит, что программировать надо учиться. Не просто выучить синтаксис и стандартную библиотеку - а как строить архитектуру, разбивать задачи на подзадачи и всё такое.

      Это не относится к конкретному языку. Здесь вы кажется превращаетесь в Д'Артаньяна. В WRI есть разные разработчики с разным опытом и уровнем как и в любой компании. Как я и говорил есть хорошие и плохие внутренние библиотеки и у меня сложилось впечатление, что внутри компании нет никакого соглашения по написанию кода. Я на самом деле даже задал этот вопрос одному из разработчиков, который там давно работает - и ответ был утвердительный. И это еще одна причина почему я написал эту статью. Возможно в будущем я ее изменю и переопубликую, чтобы она была более строгой.


      1. Refridgerator
        16.01.2024 10:43

        Это очень спорный вопрос

        Конечно, это просто личное ощущение, основанное на нумерации списков с единицы и примеров кода в математических статьях (когда он там есть) именно на фортране. Да даже пару человек практически лично знаю, которые до сих пор на фортране пишут.

        Вот тут я не совсем понимаю, что вы имеете ввиду под костылями

        Опять же, это просто моё личное восприятие с точки зрения программиста на asm/pascal/c++/SQL. Возможно, что для человека с другим бэкграундом такое решение наоборот будет выглядеть более естественным.


        1. KirillBelovTest Автор
          16.01.2024 10:43

          Там не только списки нумеруются с единицы. Все выражения нумеруются с единицы, а заголовок - это нулевой элемент. Это очень удачно выглядит при экспорте в ExpressionJSON

          ExportString[f[a, b, c], "ExpressionJSON"]
          
          Out[] = "[
          	\"f\",
          	\"a\",
          	\"b\",
          	\"c\"
          ]"
          

          То есть можно рассматривать любое выражение как плоский список в lisp, где заголовок - это нулевой элемент.


          1. Refridgerator
            16.01.2024 10:43

            А можно рассматривать и по-другому, с точки зрения ООП, где заголовок - это тип и в список вообще не входит.


            1. JerryI
              16.01.2024 10:43
              +1

              ООП в WL это не нативная структура, а эмуляция написанная поверх паттерн матчинга. В этом смысле представление, что 0-элемент это "голова" более подходящее, так как хорошо подходит под свойства языка, что код и данные - одно и то же.

              Но бывает, когда пишешь на двух-трех языках, то вечно где-то вместо 0 и 1 пишется и наоборот


      1. Refridgerator
        16.01.2024 10:43

        Легкость освоения на первоначальном этапе и сложность на последующих сыграли с WL злую шутку. Очень многие остановились на возможности построить график и взять интеграл

        Есть и другая причина - глубокое знание WL не пригодится для зарабатывания денег, лучше эти же усилия потратить на шарп с питоном. Я вполне сознательно остановился на погружении в недра языка именно поэтому. А сейчас так и вообще интереснее свой вариант языка сделать, для личных нужд. Хочу \sin x писать вообще без скобочек! И непарный разделитель в математических выражениях использовать. И массивы с нуля нумеровать. И нормальную поддержку гиперкомплексных чисел сделать.