В комментариях к мой статье пользователь @Refridgeratorв ответ на мой вопрос написал, что в Wolfram Language (WL) не хватает следующего:
ООП, перегрузки операторов, строгой типизации, событийно-ориентированного программирования, дата-ориентированного программирования, параллельного программирования с примитивами синхронизации, средств отладки, скорости исполнения. (с) @Refridgerator
Я отлично понимаю, что вокруг Mathematica сложились некоторые исторические стереотипы. В них обычно WL представляется как калькулятор на стероидах или просто игрушка, или больше язык запросов, которым можно дополнительно решать уравнения и строить графики. Сегодня я попытаюсь показать, что в языке есть не только лишь графики, уравнения и интегралы. Вряд ли у меня хватит сил написать подробно касательно каждого пункта списка, но я постараюсь объяснить хотя бы часть.
TLDR
В статье много текста, если вы не хотите заморачиваться и хотите просто пощупать то, о чем я буду писать ниже - то более короткие и простые инструкции есть в репозитории пакета Objects и на странице в Paclet Repository.
Изменяемые объекты
Как все мы прекрасно знаем WL следует принципу неизменяемых объектов. Единственный способ изменить выражение (а не создать целиком новое) это использовать функцию Part
и заменить часть выражение, которое хранится в собственных значениях символа. Если сказать человеческим языком - то мы просто можем создать переменную, которая хранит сложный объект, и уже этот объект менять. Это довольно сильное отличает WL от таких языков как Java, C#, Python и JavaScript накладывает свои ограничения, но имеет свои плюсы. Мне в принципе нравится эта концепция, но всегда хотелось иметь возможность писать код чем-то похожий на тот, что можно написать на Python. Не так давно изменяемые объекты были добавлены в язык, но они настолько неудобны, что я ими очень мало пользуюсь (да и свои объекты я сделал раньше). Изменять голые символы напрямую тоже неудобно. Ну и по итогу у меня созрела идея реализовать свои собственные объекты - их-то я сейчас и покажу, а потом расскажу, как они работают.
Во-первых, нужно иметь одну из последних версий WL. Далее нужно установить пакет из Paclet Repository, который называется довольно незатейливо - Objects:
PacletInstall["KirillBelov/Objects"]
И сразу импортируем его:
Get["KirillBelov`Objects`"]
После импорта нам доступны следующие функции:
?KirillBelov`Objects`*
CreateType
- создает изменяемый типObject
- самый примитивный изменяемый объектObjectQ
- функция, которая проверяет является ли выражение изменяемым объектомTypeQ
- функция, которая проверяет является ли выражение изменяемым типом
Начнем с базового типа для всех изменяемых объектов. Чтобы создать объект нужно просто вызвать "конструктор":
obj1 = Object[]
На самом деле это просто функция, которая возвращает объект с тем же самым заголовком, но при этом в нем появляется внутренняя структура. В этом плане я сделал нечто похожее на объекты в JavaScript, где точно также конструктором является функция, которая возвращает object
. Обратите внимание, что в напечатанном в выходной ячейке объекте перечислены его свойства. Я предусмотрел возможность задать этим свойствам конкретные значения в конструкторе при помощи синтаксиса опций, который работает как и в большинстве функций Wolfram Language:
obj2 = Object["Icon" -> Graphics3D[Sphere[], Boxed -> False]]
По умолчанию все свойства объектов имеют значение Automatic
, если для них не было задано значение при определении самого типа.
Инкапсуляция
Один из принципов, который позволяет реализовать ООП. Здесь я должен отметить, что я не следовал строго всем принципам. В первую очередь я смотрел на то, как что-то работает в других языках, таких как Python, Java, C# и JavaScript и переносил это на изменяемые объекты в том виде, в каком лично мне показалось наиболее удобным для WL. Поэтому здесь не будет классической инкапсуляции как в Java и C#, а будет неполноценная инкапсуляция без сокрытия, т.е. такая как в Python. Ведь все объекты на самом деле это выражения, а мы можем получить доступ к любой части любого выражения без каких-либо проблем. Поэтому объекты содержат свойства, но их нельзя сделать приватными. С другой стороны такому языку как Python это не мешает. Пусть сокрытие и не работает, но главная цель инкапсуляции выполнена - теперь все свойства объекта заключены внутри него самого и доступ к их значениям можно получить только непосредственно через объект. Таким образом мы можем строить сложные объекты, а на более высоких уровнях выстраивать их взаимодействие не задумываясь над тем, как они реализованы внутри. Как же работать со свойствами?
Вот так можно свойство извлечь:
obj1["Icon"]
Вот так свойство можно изменить:
obj1["Icon"] = ColorNegate[obj1["Icon"]]
obj1
Можно добавить новое свойство так, как это делается в JavaScript:
obj1["Name"] = "Object 1"
obj1
А еще не обязательно использовать строки для обращения к свойствам. Всегда можно писать символы и они автоматически будут конвертироваться в строчные имена свойств:
obj1 @ Name
obj1 @ Name = "New name for the object"
obj1 @ Name
obj1 @ NewProperty = {1, 2, 3}
obj1
На самом деле все то, что я показал выше уже очень подозрительно похоже на Association
- встроенную структуру данных в WL и аналог dict
из Python. Так и есть - объекты действительно работают с использованием этой структуры, но ассоциация сама по себе является неизменяемой и всегда копируется целиком. А объекты изменяемые и можно создать много ссылок на один объект и мутировать его из разных мест. Ниже наглядная демонстрация различий:
obj1 = Object[];
obj2 = obj1;
obj1["Icon"] = ColorSeparate[obj1["Icon"], "L"];
{obj1, obj2}
Если я попытаюсь проделать все тоже самое с ассоциациями:
assoc1 = <|"Name" -> "Assoc1"|>;
assoc2 = assoc1;
assoc1["Name"] = "New Assoc1";
{assoc1, assoc2}
То переменная, в которую была скопирована ассоциация никак не изменится. Я ссылаюсь на Python и вот как подобный код будет работать в нем:
dict1 = {"Name": "Dict 1"};
dict2 = dict1;
dict1["Name"] = "New Dict 1";
print([dict1, dict2])
Более того, мне даже не нужно создавать "ссылку" на объект чтобы работать с ним. Я могу просто создать объект в ячейке и не сохранять его в переменную вот так:
Object[]
А затем я могу просто скопировать его мышкой, вставить в новую ячейку и проделать все тоже самое:
Не смотря на то, что я все еще могу копировать SummaryBox, где иконка красная - это уже измененный объект. Просто его отображение в UI Mathematica статическое и скопировав в буфер обмена формочку SummaryBox с красной иконкой - такой она и останется, а вот объект в памяти - нет. Ведь SummaryBox - это просто отображение в блокноте, а за ширмой на самом деле прячется всего лишь выражение вот такого вида:
Object[KirillBelov`Objects`Object`$15]
Еще одним важным свойством свойств (простите за каламбур) является то, что их можно задавать с отложенным выполнением. По сути это аналог геттеров из Java или C#. Вот как это можно сделать:
obj1["Random"] := RandomReal[]
{obj1["Random"], obj1["Random"], obj1["Random"]}
Каждый раз при обращении к такому свойству мы будем вычислять правую часть заново.
Наследование
Какой толк от мутируемых объектов, если из них нельзя выстроить иерархию типов. Для того чтобы создать новый тип-потомок от какого-то существующего типа я добавил функцию CreateType
. Вот как ее можно использовать:
(* CreateType[type, parent, init, {fields}] *)
CreateType[Human, Object, Identity,
{"Name", Age, "Country" -> Entity["Country", "Russia"]}]
Kirill = Human[]
Обратите внимание, что в списке "Properties"
появились как имена свойств из Object
, так и новые, которые мы только что задали типу. Т.е. свойства были наследованы!
Обязательным аргументом функции CreateType
является только первый - это должен быть символ, который обозначает новый тип. Все остальные аргументы по умолчанию:
parent
- по умолчанию наследование отObject
init
- по умолчанию этоIdentity
- по сути функция ничего не делаетfields
- по умолчанию пустой список{}
Стоит отменить, что родительский тип обязан быть уже существующим типом, а свойства указываются в виде списка правил или списка имен. Там где вместо имени правило (как для "Country"
), то правая часть будет использована по умолчанию. Там где только имя (строка или символ) - по умолчанию значения свойства будет Automatic
. Вообще странно, что я создал тип "человек", но у него осталась старая иконка с рисунком Спайки. Можно это как-то изменить? Конечно! Для любого свойства родительского типа можно переопределить значение по умолчанию вот так:
CreateType[Human, {
"Name",
Age,
"Country" -> Entity["Country", "Russia"],
"Icon" -> Style["????♂️", 22]
}]
Kirill = Human["Name" -> "Kirill Belov"]
С наследованием свойств мы разобрались. Конечно, там есть еще тонкости, но перейдем к более важным вопросам. А как же методы? Где они вообще? Ведь про них пока что не было ни слова! Вот сейчас-то мы до них и добрались. Сначала немного пространных рассуждений.
В Wolfram Language существует несколько способов создать и сохранить определение. От способа зависит во первых место, куда определение будет сохранено, а во вторых символ, с которым определение будет связано. Когда мы создаем стандартную функцию вот так:
func1[x_, y_] := x^2 + y^3 - 1
То определение функции связывается с именем func1
. Теперь оно будет хранится в DownValues
для этого символа:
DownValues[func1]
И естественно мы можем создать функцию, которая определена на нашем объекте и изменяет его каким-то образом. Допустим, мы хотим, чтобы созданный нами человек умел что-то говорить, например, слово "привет" и называть свое имя. Вот такую функцию можно для этого создать:
sayHi[human_Human] := Echo[
StringTemplate["Hi! My name is `1`"][human["Name"]],
Row[{human["Name"], " says: "}]
]
sayHi[Kirill]
Обратите внимание, что я указал шаблон human_Human
, где после знака подчеркивания идет заголовок выражения. Это позволяет точно указать тип, на котором будет работать функция. С любым другим типом она просто не будет работать и вернет обратно весь Input. Таким образом мы можем использовать сильную типизацию в аргументах функций, т.е. такую типизацию, где отсутствует неявное приведение типов. Это значит, что наша функция будет работать на типе Human
и только на нем. А как же наследование? Давайте создадим производный тип, например студента и попробуем вызвать ту же самую функцию на нем:
CreateType[Student, Human, {"Course" -> 1}]
Ivan = Student["Name" -> "Ivan"]
sayHi[Ivan]
Как видно выше, если даже человек умеет говорить "привет", то студент такой способностью обделен. Хотя он полностью унаследовал все свойства человека, но функции определенные на нем - нет. В чем же дело? Дело в связывании. Функция sayHi
имеет глобальное определение и сильную типизацию. На самом деле тип Human
вообще ничего не знает о том, какие функции были на нем определены. Это не так плохо, как кажется на первый взгляд, так как дает выбор не таскать за каждым объектом кучу определений, которые ему не нужны, а создать что-то конкретное для типа не изменяя сам тип. В общем по этому поводу можно долго рассуждать. Самое главное я показал проблему и объяснил ее. Есть ли решение? Естественно! Кроме связывания определения с функцией, мы так же можем связать его и с самим типом при помощи UpValues! Чтобы создать такое определение нужно использовать чуть-чуть другой синтаксис, где нужно указать собственно символ-тип, к которому определение нужно привязать:
ClearAll[sayHi]
Human /: sayHi[human_Human] := Echo[
StringTemplate["Hi! My name is `1`"][human["Name"]],
Row[{human["Name"], " says: "}]
]
sayHi[Kirill]
Пока что все работает в точности так же. Только синтаксис создания функции (теперь это можно назвать методом) чуть-чуть изменился. В самом начале мы указали Human /:
чтобы показать к чему привязывается определение. Это сработает ТОЛЬКО
если определение имеет вид f[x_MyType] := ..
, т.е. чтобы сам объект был на первом уровне в ТЕЛЕ
шаблона и только там. Т.е. вот так уже не сработает:
Human /: human_Human[] := {} (* тут Human на уровне 0 *)
Human /: f[g[human_Human[]]] := {} (* тут на уровне 2 *)
Определение созданное таким образом было привязано не к функции, а с типу Human
. Вот как это можно проверить:
Cases[UpValues[Human], _[_[sayHi[_]], _]]
И теперь, если мы снова создаем тип Студент, то определение sayHi
наследуется:
CreateType[Student, Human, {"Course" -> 1}]
Ivan = Student["Name" -> "Ivan"]
sayHi[Ivan]
Таким образом мы можем легко выстроить иерархию типов, где экземпляры будут хранить только свои собственные данные, а определения будут привязаны к символу соответствующему типу, но что самое важное и свойства и методы наследуются при создании потомков.
Полиморфизм
Полиморфизм в WL отличается от того как он реализован в других языка, которые я уже неоднократно перечислял. Во-первых, если брать опять же Python, Java, C# и JavaScript, то ни один из этих языков не имеет сильной типизации. У Python и JS ее вообще нет, а Java и C# имеют слабую типизацию. При этом первая пара имеет динамическую типизацию, а вторая - статическую. WL при этом имеет сильную динамическую типизацию там, где это возможно.
Я еще раз хочу обратить внимание на то, что классификация языков программирования и обсуждение парадигм и подходом очень холиварная тема и я люблю ее ее пообсуждать и буду рад, если кто-нибудь в комментариях поделится своим мнением. Поэтому, чтобы дать дополнительную пищу для холивара, я должен уточнить, что сильная и слабая типизация в контексте статьи означает отношение функции и аргументов (ведь в WL ничего другого нет), а динамическая и статическая типизация относится к способу выделения памяти для объектов. В Java и C# это можно сделать только один раз под конкретный тип и даже var
и dynamic
не помогут перезаписать уже сохраненное значение на значение с другим типом. JS и Python же наоборот позволяют в любую переменную записать все что угодно. Получается в WL переменной можно в любое мгновение динамически менять тип, но при этом функции всегда проверяют тип аргументов и не делают неявное приведение.
Собственно перейдем к особенностям полиморфизма. В предыдущих разделах я говорил, что часть функционала моего пакета похожа на JS, а часть на Python и немного на C#. Мы знаем, что C# и Java легко поддерживают перегрузку методов, а Python насколько я понимаю делает это через костыли с атрибутами по итогу создавая определение с условиями. В JS перегрузки вообще нет - нужно в ручную проверять в условных операторах аргументы. Если я не прав - искренне надеюсь меня поправят.
Перегрузка
Но как работает перегрузка в WL? (Я должен уточнить, что это не является особенностью конкретно моего пакета, а язык все это умеет по умолчанию). Больше всего перегрузка функций похожа на Java и C#. Но есть одно важное отличие. Допустим я могу создать класс и два метода и одним именем, но разными параметрами:
public class Human {
public string Name { get; set; }
public string sayHi(){
return $"Hi! My name is {Name}";
}
public string sayHi(string postfix){
return $"Hi! My name is {Name}. {postfix}";
}
}
new Human {Name = "Kirill Belov"}.sayHi();
new Human {Name = "Kirill Belov"}.sayHi("And what is your name?");
Что здесь произошло на самом деле? В таблице методов CLR для этого класса создается два определения, у которых отличаются сигнатуры - т.е. имя метода + типы и количество аргументов. Можно ли сделать что-то подобное в WL? Конечно да!
Human /: sayHi[human_Human, postfix_String] :=
Echo[
StringTemplate["Hi! My name is `1`. `2`"][human["Name"], postfix],
Row[{human["Name"], " says: "}]
]
sayHi[Kirill]
sayHi[Kirill, "What is your name?"]
Получилось в точности тоже самое, что в примере с C#. Но это еще не все. Дело в том, что определения в WL не используют сигнатуры. Они используют шаблоны. А шаблоны являются целым внутренним языком. Это похоже на паттерн-матчинг из функциональных языков, только паттерны можно напрямую указывать в определении функции, а не проверять в теле.
Например вот так я могу создать функцию, где шаблон будет значительно сложнее чем просто сигнатура:
Human /: addPets[human_Human, pets: {{_String, _}..}] /;
AssociationQ[human["Pets"]] :=
Table[With[{petName = pet[[1]]}, human["Pets", petName] = pet], {pet, pets}]
Human /: addPets[human_Human, pets_] := (
human["Pets"] = <||>;
addPets[human, pets]
)
addPets[Kirill, {{"Robet", Dog[]}, {"Jessica", Cat[]}}]
Kirill["Pets"]
Что мы видим выше? Поиск определения в списке происходит не просто по сигнатуре. В данном случае аргумент сначала сравнивается с шаблоном {{_String, _}.. }
, который означает список списков, где на втором уровне два элемента и первый - строка. Дополнительно после это сравнения происходит проверка, что у объекта human
есть свойство "Pets"
и оно является ассоциацией. Такой способ создания определений намного более гибкий чем использование сигнатур. Но есть и свои минусы - поиск функций по шаблонам медленнее чем по сигнатурам. Кроме того, что мы можем расширить сигнатуру шаблоном - т.е. превратить во что-то более сложное по структуре, мы еще можем и сузить сигнатуру. Типичная сигнатура указывает, что функция принимает аргумент определенного типа, т.е. это некий диапазон возможных значений. Шаблон же может быть в том числе и одним конкретным значением. Это значит что я могу создать вот такую функцию-метод:
Human /: getName[human_Human, "first"] :=
StringSplit[human["Name"]][[1]]
Human /: getName[human_Human, "last"] :=
StringSplit[human["Name"]][[-1]]
getName[Kirill, "first"]
getName[Kirill, "second"]
getName[Kirill, "last"]
То есть в примере выше шаблон соответствует точному значению, а не значениям какого-либо типа. Даже с учетом того, что во всех трех случаях я передал вторым аргументом строку.
Переопределение
С перегрузкой мы разобрались, хоть это только небольшая часть возможностей. Но что же с переопределением? То есть можно ли создать функцию-метод с поведением отличным от того же метода родителя? Как обычно ответ - да. Иначе я бы про это не писал. Переопределение тоже довольно очевидно, особенно после того как выше я продемонстрировал, что можно переопределить значения по умолчанию в типа-потомках. Т.е. если мы создали потомка и у его родителя есть какой-то метод, который был наследован - мы можем просто переопределить его в лоб:
CreateType[Student, Human, {"Course" -> 1}]
Student /: getName[student_Student, "last"] :=
StringTemplate["Call me just `1`. Let's not be formal."][getName[student, "first"]]
Ivan = Student["Name" -> "Ivan"]
getName[Ivan, "first"]
getName[Ivan, "last"]
Обратите внимание, что определение для "first"
сохранилось и наследовалось от Human
! Даже с учетом того, что сигнатуры одинаковые и для одной перегрузки мы создали определение, а для второй нет. Все тоже самое сработает и с условиями, и с проверками, и с альтернативами!
Реальные примеры
Статья уже получилась довольно длинной. И боюсь ее сложно будет читать. Поэтому на сегодня я закончу обзор ООП в Wolrfam Language, но прежде всего хочу показать, что все то, о чем я писал выше не просто игрушка, а используется мной и моими товарищами в наших пет-проектах. Именно эту реализацию изменяемых объектов я постепенно добавляю во многие свои проекты и она работает замечательно, хоть в ней есть далеко не все что я запланировал. Вот например код, который используется в библиотеке TelegramBot:
Обратите внимание, что конструктор телеграм-бота переопределен. В качестве параметра он теперь принимает еще и строку с уникальный токеном бота.
А вот еще пример использование объектов в TCPServer, который работает полностью на WL:
Кстати если внимательно посмотреть на скриншот, то видно, что в самом коде обработки пакета вызывается множество специфических функций - это все методы сервера, которые определены ниже.
У меня еще много примеров и мыслей, которыми я бы хотел поделиться. Плюс я прошелся не по всем пунктам комментария из самого начала. Я отлично знаю, что тема парадигм программирования и подходов к программированию может вызвать интересную дискуссию, так как эту тему любят на Хабре. Буду рад выслушать мнение уважаемых читателей!
Всем спасибо за внимание!
Комментарии (12)
Refridgerator
16.11.2023 09:14+2В одном из проектов у меня используется перегрузка деления, чтобы объединять объекты в коллекции без использования скобок, например:
box+=mp3/delay/waveout;
Но у Вольфрама последовательное деление автоматически преобразуется и получаем
Как быть в этом случае?
JerryI
16.11.2023 09:14+1Я думаю надо перегрузить Times и Power или Superscript.
вероятно Кирилл лучше знает...то что придумал
myObj /: Times[myObj[t1_], Power[myObj[t2_], -1]] := collection[myObj[t1], myObj[t2]]
myObj["mp3"] / myObj["wave"]
но в принципе можно и без врапперов
myObj
KirillBelovTest Автор
16.11.2023 09:14+1Ну если честно это какой-то антипаттерн. Синтаксис такой, какой он есть и конечно лучше бы ему следовать. У меня например была перегрузка точки, чтобы свойства объектов можно было получать и назначать вот так
obj1.Name
иobj1.Name = "Name"
, но отказался от этого чтобы не ломать лишнего.И так к перегрузке деления. В WL нет никакого деления и сделать конкретно то, что вы показали выше чуть-чуть сложнее чем кажется, но возможно:
box = MyBox[{}] MyBox /: AddTo[box_MyBox, Times[a_, p : (Power[_, -1] ..)]] := MyBox[Join[box[[1]], {a}, {p}[[All, 1]]]] MyBox /: Set[name_Symbol, box_MyBox] := ( name /: AddTo[name, v_] := With[{b = name}, name = AddTo[b, v]]; Block[{MyBox}, name = box] ) box += a/b/c box += e/f/g/h
KirillBelovTest Автор
16.11.2023 09:14только надо чтобы In[151] выполнялся после In[153], я выполнил несколько раз - поэтому у меня сработало
Refridgerator
16.11.2023 09:14Ну вот, нужно переопределить одно, а переопределяем совсем другое. Если дополнительно к этому потребуется переопределить верхний индекс (который в математике не обязательно возведение в степень) - будет ещё интереснее. Так и получается, что даже для простых задач приходится изворачиваться, костылить, и тратить совершенно непропорциональное количество времени.
на c# выглядит более дружелюбно к читателю вроде бы
#region Collector public static ModuleCollector operator /(Module module1, Module module2) { ModuleCollector mc = new ModuleCollector(); mc.Modules.Add(module1); if (module2 != module1) mc.Modules.Add(module2); return mc; } public static ModuleCollector operator /(ModuleCollector mc, Module module) { if (!mc.Modules.Contains(module)) mc.Modules.Add(module); return mc; } public static ModuleCollector operator /(Module module, ModuleCollector mc) { if (!mc.Modules.Contains(module)) mc.Modules.Add(module); return mc; } #endregion
Про антипаттерн не согласен. Всё-таки исторически символ деления - это двоеточие или горизонтальная черта. А прямой слэш используется для других целей не менее редко, в том числе и для перечисления.
KirillBelovTest Автор
16.11.2023 09:14Я все таки за то, чтобы была точность в формулировках. Когда вы говорите «хочу переопределять одно, а переопределяли другое» это неверно. Этого «одного» вообще нет в языке. В WL нет операторов и в том числе нет оператора деления. Это такой синтаксический Сахар для записи математических выражений. Если бы вы хотели переопределять то, что существует в том виде в каком записывается - это делает намного проще чем я показал и чем это сделано в c#. Например Plus или Times
Но ведь можно предложить и обратную ситуацию. Как в C# переопределять «//.» Или «/*» или “@@@“или нижний индекс?
Refridgerator
16.11.2023 09:14В WL нет операторов и в том числе нет оператора деления
Так мы же с этого вроде бы и начинали - чего мне не хватает, чтобы использовать Вольфрам как языка общего назначения) Умножение-то есть, хоть и как функция, и символ слеша тоже есть, и тоже для описания именно деления.
Просто скопировал из Вольфрама и вставил сюда, дробь на слеш он поменял самостоятельно, равно как и верхний индекс
F/:F[a_,x_]+F[b_,z_]:=F[a+b,x+z] F/:F[a_,x_]-F[b_,z_]:=F[a-b,x-z] F/:F[a_,b_]*F[c_,d_]:=F[ac+bd,bc+ad+bd] F/:F[a_,b_]/F[c_,d_]:=F[(-bd+a(c+d))/(c^2+cd-d^2),(bc-ad)/(c^2+cd-d^2)] F/:F[a_,b_]**F[c_,d_]:=F[ac-bd+ad,bc-ad] F/:F[a_,b_]^n_:=Product[F[a,b],{k,1,n}]
Это Фибоначчионы, новое слово в математике)
Но ведь можно предложить и обратную ситуацию. Как в C# переопределять «//.» Или «/*» или “@@@“или нижний индекс?
Никак, но ведь эти символы и не являются общепризнанными с однозначными трактовками их смысла? Зато в с++ есть текстовый препроцессор, на котором некоторые даже производные умудрялись считать в аналитическом виде.
KirillBelovTest Автор
16.11.2023 09:14Я правильно понимаю, что в C# есть оператор деления и для его переопределения вы написали класс с тремя методами. Более читабельно там только то, что в сигнатуре написано “operator /“. Плюс это конкретный класс, который может использовать только конкретные типы, а если делать обобщенный тип, то использовать его будет еще чуть-чуть сложнее. И последнее - вы не написали переопределение для “+=“, а ведь это еще один метод.
Для того же самого в WL я сделал две функциии, которые одновременно изменили поведение AddTo/Power/Times. В принципе это можно было бы и в одном определении сделать. Но я сделал так, чтобы при применении к выражению возвращалось новое выражение, а при применении к символу - изменялся сам символ. И все это с учетом того, что оператора или функции деления в WL нет.
Далее наоборот. Я предложил переопределить оператор «которого нет» в C#, вы сказали это невозможно.
Так в итоге в чью пользу складывается «зачет» по «гибкости синтаксиса»?
В общем я понимаю всю непродуктивность этого спора, да я и не спорю - просто продолжаю дискуссию. Вы спросили как перегрузить деление, а я показал. Вам этот способ показался не таким красивым вот и все. На мой взгляд там все довольно хорошо получилось, хотя опять же я понимаю, что далеко не каждый пользователь WL способен сам до такого додуматься - ну например до того, чтобы делать вложенный UpSet во время присваивания. И вот эта сложность может отпугивать.
И на счет символа деления. Не знаю ни одного популярного языка программирования, который использует двоеточие в качестве оператора деления или умеет использовать горизонтальную черту. Горизонтальную черту только математические пакеты используют в лучшем случае.
В общем в очередной раз спасибо вам за обсуждение. Мне с вами очень интересно беседовать. Если можете - поделитесь пожалуйста ссылкой на свои проекты на GitHub или еще где-нибудь, которые связаны с WL. Я всегда в поисках интересного кода и с большим интересом изучаю другие проекты.
KirillBelovTest Автор
16.11.2023 09:14И еще. Если у вас действительно есть желание использовать в каких-то проектах WL как язык общего назначения, но что-то останавливает - это явно не должен быть синтаксис. Если у вас есть еще какие-то проблемы, с которыми вы столкнулись и в итоге отбросили WL, то я с радостью про них почитаю и подумаю есть ли решение. (Ну кроме того, что вы просто сами не хотели). Я постепенно изучал его и обнаружил, что по сути там не один порог входа, а два. Первый очень низкий. Куда ниже чем у большинства технологий. Просто запустил, посмотрел короткие туториалы и вот ты уже почти на естественном языке решаешь уравнения и берёшь интегралы.
Второй порог входа как раз выше чем в среднем, так как там очень мало руководств и примеров действительно больших проектов и того, как эти большие проекты делать правильно. Даже если посмотреть код самой математики или посмотреть репозитории WRI - там в основном просто жуткая мешанина и говнокодище. Всякий раз когда я лезу смотреть исходники - я просто в ужасе. Так что неудивительно, что те кто говнокодит сам не делает хорошие руководства для разработки крупных проектов.
Refridgerator
16.11.2023 09:14Прошу прощения, я не хотел, чтобы эта дискуссия приобрела негативную эмоциональную окраску. Переопределения += в моём примере не было, потому что он определён только для другого, более высокоуровнего типа, и по синтаксису ничем не отличается.
Задачу, которую мне не удалось решить в Вольфраме - это DSP-процессор. В более простых вариантах - спектральный анализатор и генератор сигналов в режиме реального времени. Возможно просто потому, что я не достаточно хороший программист. Но мне же удалось её решить и на c#, и на c++.
скриншот
Refridgerator
Оперативно! Спасибо за статью.
KirillBelovTest Автор
Чем мог! ????
Когда смогу постараюсь высказать свои мысли про остальное