Pull to refresh

Comments 28

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


Это в духе 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]

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

func
var
someVariable
someFunction

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

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

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

Module.wl

BeginPackage["ContextName`"]

Begin["`DontLookAtMe`"]

SomeVariable = 777;
SomeFunction[_] := RandomInteger[{0,10}];

End[] 

EndPackage[]

{ContextName`DontLookAtMe`SomeFunction, ContextName`DontLookAtMe`SomeVariable}

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

BeginPackage["ContextName`"];

SomeVariable;

SomeFunction;

Begin["`DontLookAtMe`"];

SomeVariable = 777;

SomeFunction[_] := RandomInteger[{0,10}];

End[]; 

EndPackage[];

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

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

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

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

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

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

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

И по второму пункту. Как я и писал в статье - любой текстовый файл с кодом на 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.

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

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

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

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

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

я изучил много встроенных пакетов в 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. Каждый раз, когда приходится с ним пересекаться, один и тот же вопрос возникает - что курили разработчики, когда придумывали его синтаксис и названия для функций? Наверно поэтому в нём овермиллион тулбоксов на все случаи жизни, чтобы избежать программирования любой ценой.

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

addOne[x_] := x + 1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

mapFunc[packedArray] (*тут сработает автокомпиляция, т.е. код выполнится быстрее*)

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

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

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

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

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

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

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

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

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

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

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

ExportString[f[a, b, c], "ExpressionJSON"]

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

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

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

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

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

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

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

Использую Wolfram Mathematica регулярно. Стараюсь немного причесывать код, чтоб он визуально был читаем. И не превращался в тех длинных монстров, примеры которых вы привели.

Но я думал, что в крупных компаниях соглашение об оформлении кода соблюдается строго! Это к той части где вы говорите о не самом лучшем оформлении кода у авторов  из Wolfram.

Кирилл, спасибо за столь подробную статью, можно даже сказать руководство. 

А в Mathematica придумали наконец какое-нибудь колдунство, которое заставляло бы массивы итерироваться с нуля, а не с единицы? Это иногда здорово мешает, особенно когда начинаешь переносить код из ноутбука во что-то более пристойное, типа плюсов - как ни следи, постоянно случается на грабли out-of-one наступить.

То есть, про трюк с использованием Subscript (или Notation, что в принципе одно и то же) я знаю, но вдруг появилось что-то более общее и универсальное?

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

List[a, b, c] (* полная форма {a, b, c}*)

List - это нулевой элемент. А вообще я например очень редко пользуюсь итераторами, так как если у меня есть список и нужно пробежаться по нему и что-то сделать - то куда проще сделать это через:

Map[func, list] (*или*)
Table[func[item], {item, list}]

Более общее и универсальное надо делать самому. Как вариант - это наоборот, в плюсах делать нумерацию с единицы (то есть после инициализации декрементировать ссылку на массив). При портировании программ с Фортрана так и делают в основном.

В принципе с помощью техники UpValues (с помощью чего вероятно сделано дифференцирование) можно написать транслятор в Си-подобные языки. А Experimental`OptimizeExpression[expr] можно вовсе превращать всякие колдунские формулы в оптимизированное выражение, которое можно напрямую в почти в Си. SymbolicC пакет может в этом помочь.

Я как-то начинал писать транслятор с автодетектом типов в OpenCL, но забросил... А в целом это можно сделать при очень большом желании.

Sign up to leave a comment.

Articles