In the comments to my article, user @Refridgerator wrote in response to my question that the Wolfram Language (WL) is missing the following:
OOP, operator overloading, strict typing, event-driven programming, data-driven programming, parallel programming with synchronization primitives, debugging tools, execution speed. (c) @Refridgerator
I’m well aware that there are some historical stereotypes around Mathematica. They usually present WL as a calculator on steroids, or just a toy, or more of a query language that can be used to additionally solve equations and graphs. Today I will try to show that there is more to the language than just graphs, equations and integrals. I hardly have enough energy to write in detail about every item on the list, but I will try to explain at least some of them.
I'm not the first to tackle this challenge. There are already functions on Mathematica Stack Exchange and elsewhere that do something similar.
TLDR
TLDR
There’s a lot of text in the article, if you don’t want to get bogged down and just want to get a feel for what I’m going to write about below - there are shorter and simpler instructions in the Objects package repository and on the page in the Paclet Repository.
Mutable objects
Mutable objects
As we all know, WL follows the principle of immutable objects. The only way to change an expression (rather than create a whole new one) is to use the Part function and replace part of the expression, which is stored in the symbol’s own values. To put it in human terms, we can simply create a variable that stores a complex object and change that object. This is quite a strong difference between WL and languages like Java, C#, Python and JavaScript, which imposes its own limitations, but has its advantages. I like the concept in principle, but I’ve always wanted to be able to write code something similar to what you can write in Python. Modifiable objects were added to the language not too long ago, but they are so inconvenient that I use them very little (and I made my own objects earlier). Modifying bare symbols directly is also inconvenient. So, as a result, I had an idea to implement my own objects - I will show them now and then tell you how they work.
First, you need to have one of the latest versions of WL. Then you need to install a package from Paclet Repository, which is called rather unpretentious - Objects:
In[]:=
PacletInstall["KirillBelov/Objects"]
Out[]=
PacletObject
And import it right away:
In[]:=
Get["KirillBelov`Objects`"]
After importing, the following functions are available to us:
In[]:=
?KirillBelov`Objects`*
Out[]=
◼
CreateType - creates a modifiable type
◼
Object - the most primitive modifiable object
◼
ObjectQ - a function that checks if an expression is a modifiable object.
◼
TypeQ - function that checks if the expression is a modifiable type
Let’s start with the basic type for all modifiable objects. To create an object you just need to call the “constructor”:
In[]:=
obj1=Object[]
Out[]=
Object
It’s really just a function that returns an object with the same header, but it gives it an internal structure. In this respect, I’ve done something similar to objects in JavaScript, where in the same way the constructor is a function that returns an object. Notice that the printed object in the output cell lists its properties. I have provided the ability to set these properties to specific values in the constructor using option syntax, which works like most Wolfram Language functions:
In[]:=
obj2=Object["Icon"->Graphics3D[Sphere[],Boxed->False]]
Out[]=
Object
By default, all object properties are set to Automatic unless a value has been set for them when defining the type itself.
Encapsulation
Encapsulation
One of the principles that allows you to implement OOP. I should mention here that I did not strictly follow all the principles. First of all, I looked at how something works in other languages such as Python, Java, C# and JavaScript and transferred it to modifiable objects in the way that I personally found most convenient for WL. That’s why there won’t be a classical encapsulation like in Java and C#, but an incomplete encapsulation without hiding, i.e. like in Python. After all, all objects are actually expressions, and we can access any part of any expression without any problems. So objects contain properties, but they cannot be made private. On the other hand a language like Python is not hindered by this. Hiding may not work, but the main purpose of encapsulation is fulfilled - now all the properties of an object are enclosed inside it and their values can only be accessed directly through the object. Thus we can build complex objects, and at higher levels build their interaction without thinking about how they are realized inside. How to work with properties?
This is how a property can be extracted:
In[]:=
obj1["Icon"]
Out[]=
This is how a property can be changed:
In[]:=
obj1["Icon"]=ColorNegate[obj1["Icon"]]obj1
Out[]=
Out[]=
Object
You can add a new property the way it is done in JavaScript:
In[]:=
obj1["Name"]="Object 1"obj1
Out[]=
Object 1
Out[]=
Object
Also, you don’t have to use strings to refer to properties. You can always write characters and they will be automatically converted to lowercase property names:
In[]:=
obj1@Nameobj1@Name="New name for the object"obj1@Nameobj1@NewProperty={1,2,3}obj1
Out[]=
Object 1
Out[]=
New name for the object
Out[]=
New name for the object
Out[]=
{1,2,3}
Out[]=
Object
In fact, what I’ve shown above is already very suspiciously similar to Association - a built-in data structure in WL and analog of dict from Python. It is - objects do work using this structure, but the association itself is immutable and is always copied in its entirety. And objects are mutable and you can create many references to a single object and mutate it from different places. Below is a visual demonstration of the differences:
In[]:=
obj1=Object[];obj2=obj1;obj1["Icon"]=ColorSeparate[obj1["Icon"],"L"];{obj1,obj2}
Out[]=
Object,Object
If I try to do the same thing with associations:
In[]:=
assoc1=<|"Name"->"Assoc1"|>;assoc2=assoc1;assoc1["Name"]="New Assoc1";{assoc1,assoc2}
Out[]=
{NameNew Assoc1,NameAssoc1}
Then the variable to which the association was copied will not change in any way. I am referring to Python and here is how such code will work in it:
In[]:=
dict1 = {"Name": "Dict 1"};
dict2 = dict1;
dict1["Name"] = "New Dict 1";
print([dict1, dict2])
dict2 = dict1;
dict1["Name"] = "New Dict 1";
print([dict1, dict2])
[{'Name': 'New Dict 1'}, {'Name': 'New Dict 1'}]
Furthermore, I don’t even need to create a “reference” to the object to work with it. I can just create an object in a cell and not save it to a variable like this:
In[]:=
Object[]
And then I can just copy it with my mouse, paste it into a new cell and do the same thing:
Even though I can still copy a SummaryBox with a red icon, it’s already a modified object. It’s just that its display in Mathematica’s UI is static, and copying a SummaryBox with a red icon to the clipboard will keep it that way, but the object in memory will not. After all, the SummaryBox is just a display in Notepad, and behind the screen is actually just an expression like this:
Another important property of properties (pardon the pun) is that they can be set with deferred execution. This is essentially analogous to getters from Java or C#. Here’s how it can be done:
Each time we access such a property, we will compute the right-hand side anew.
Inheritance
Inheritance
What good are mutable objects if you can’t build a type hierarchy from them. In order to create a new type-descendant from some existing type, I added the CreateType function. Here’s how you can use it:
Note that in the “Properties” list, both the property names from Object and the new ones we just set to the type appeared. That is, the properties were inherited!
Only the first argument to the CreateType function is mandatory - it must be a character that denotes the new type. All other arguments are default:
◼
parent - inherit from Object by default
◼
init - by default it is Identity - in fact the function does nothing
◼
fields - by default an empty list {}
It should be noted that the parent type must be an existing type, and the properties are specified as a list of rules or a list of names. Where there is a rule instead of a name (as for “Country”), the right-hand side will be used by default. Where only the name (string or character) - the default property value will be Automatic. In general it’s strange that I created the type “person”, but it still has the old icon with Spikey’s drawing. Is there any way to change it? Sure! For any property of the parent type you can override the default value like this:
We have dealt with property inheritance. Of course, there are still some subtleties, but let’s move on to more important issues. What about methods? Where are they? We haven’t heard a word about them yet! Now we’re getting to them. First, a little bit of lengthy reasoning.
There are several ways to create and save a definition in the Wolfram Language. The method depends first on where the definition is saved, and second on the symbol to which the definition is associated. When we create a standard function like this:
That function definition is associated with the name func1. It will now be stored in DownValues for this symbol:
And naturally we can create a function that is defined on our object and changes it in some way. Let’s say we want the person we created to be able to say something, for example, the word “hello” and say his name. This is the kind of function we can create for that:
Note that I have specified the human_Human template with the expression header after the underscore. This allows you to specify the exact type on which the function will work. With any other type it simply won’t work and will return all Input back. Thus, we can use strong typing in function arguments, i.e. typing where there is no implicit type conversion. This means that our function will work on the Human type and only on it. What about inheritance? Let’s create a derived type, for example student, and try to call the same function on it:
As we can see above, if even a human being knows how to say “hello”, the student is deprived of this ability. Although he has completely inherited all the properties of a human being, but the functions defined on him are not. What is the matter? The point is in binding. The sayHi function has a global definition and strong typing. In fact, the Human type knows nothing at all about what functions have been defined on it. This is not so bad as it seems at first sight, as it gives you a choice not to carry a bunch of definitions for each object, which it does not need, but to create something specific for the type without changing the type itself. In general, we can argue about it for a long time. The most important thing is that I have shown the problem and explained it. Is there a solution? Naturally! Besides linking a definition with a function, we can also link it with the type itself using UpValues! To create such a definition, we need to use a slightly different syntax, where we need to specify the actual symbol-type to which the definition should be bound:
So far, everything works exactly the same. Only the syntax of creating a function (now we can call it a method) has changed a bit. At the very beginning we specified Human /: to show what the definition is bound to. This will work ONLY if the definition has the form f[x_MyType] := ..., i.e. so that the object itself is at the first level in the body of the template and only there. I.e. it won’t work like that anymore
The definition created in this way was not bound to a function, but to a Human type. Here is how to check it:
And now, if we create the Student type again, the definition of sayHi is inherited:
In this way we can easily build a hierarchy of types, where instances will only store their own data, and definitions will be bound to the symbol corresponding to the type, but most importantly both properties and methods are inherited when creating descendants.
Polymorphism
Polymorphism
Polymorphism in WL is different from how it is implemented in the other languages I’ve already listed many times. First of all, if we take Python, Java, C# and JavaScript again, none of these languages have strong typing. Python and JS have none at all, and Java and C# have weak typing. At the same time, the first pair has dynamic typing and the second pair has static typing. WL has strong dynamic typing where possible.
I would like to point out once again that the classification of programming languages and the discussion of paradigms and approach is a very holivorous topic and I like to discuss it and would be glad if someone in the comments shares his/her opinion. So, to give additional food for holivar, I should clarify that strong and weak typing in the context of the article means the relation of function and arguments (because there is nothing else in WL), and dynamic and static typing refers to the way of allocating memory for objects. In Java and C# this can be done only once for a specific type and even var and dynamic will not help to overwrite an already stored value with a value of a different type. JS and Python, on the contrary, allow you to write anything to any variable. It turns out that in WL you can dynamically change the type of a variable at any moment, but at the same time functions always check the type of arguments and do not do implicit conversion.
Let’s move on to the peculiarities of polymorphism. In the previous sections I said that some of the functionality of my package is similar to JS, and some of it is similar to Python and a little bit to C#. We know that C# and Java easily support method overloading, while Python as far as I understand does it through crutches with attributes, eventually creating a definition with conditions. In JS there is no overloading at all - you need to manually check arguments in conditional operators. If I am wrong - I sincerely hope to be corrected.
Overload
Overload
But how does overloading work in WL? (I should clarify that this is not a feature of my package specifically, and the language knows how to do it all by default). Function overloading is most similar to Java and C#. But there is one important difference. Let’s say I can create a class and two methods with the same name but different parameters:
What really happened here? The CLR method table creates two definitions for this class that have different signatures - i.e. method name + types and number of arguments. Is it possible to do something like this in WL? Of course yes!
It turned out exactly the same as in the C# example. But that’s not all. The thing is that WL definitions don’t use signatures. They use patterns. And patterns are an entire internal language. It’s like pattern-matching from functional languages, only patterns can be directly specified in the function definition instead of being checked in the body.
For example like this I can create a function where the pattern will be much more complex than just a signature:
What do we see above? The definition in the list is not simply searched by signature. In this case, the argument is first compared to the pattern {{_String, _}.. }, which means a list of lists with two elements on the second level and the first one being a string. Additionally, after this comparison, it is checked that the human object has the property “Pets” and that it is an association. This way of creating definitions is much more flexible than using signatures. But there are some disadvantages - searching for functions by patterns is slower than by signatures. Besides the fact that we can extend a signature with a template - i.e. turn it into something more complex in structure - we can also narrow the signature. A typical signature indicates that a function takes an argument of a certain type, i.e. it is a range of possible values. A pattern can be one specific value. This means that I can create such a function-method:
That is, in the example above, the pattern matches the exact value, not values of any type. Even taking into account that in all three cases I passed a string as the second argument.
Overriding
Overriding
We’ve dealt with overloading, although it’s only a small part of the possibilities. But what about overriding? That is, is it possible to create a function-method with behavior different from the same method of its parent? As usual, the answer is yes. Otherwise I wouldn’t have written about it. Overriding is also pretty obvious, especially after I demonstrated above that you can override default values in descendant types. I.e. if we created a descendant and its parent has some method that was inherited - we can just override it head-on:
Note that the definition for “first” is preserved and inherited from Human! Even though the signatures are the same and we have created a definition for one overload and no definition for the other. Everything will work the same with conditions, checks, and alternatives!
Real examples
Real examples
This article is already quite long. And I’m afraid it will be hard to read. So today I will finish the review of OOP in Wolrfam Language, but first of all I want to show that all the things I wrote above are not just a toy, but are used by me and my friends in our pet projects. It is this implementation of changeable objects that I am gradually adding to many of my projects and it works great, even though it doesn’t have everything I planned. Here is for example the code used in the TelegramBot library:
Note that the telegram bot constructor has been redefined. It now accepts a string with the unique token of the bot as a parameter.
By the way, if you look carefully at the screenshot, you can see that the package processing code itself calls a lot of specific functions - these are all server methods, which are defined below.
I have many more examples and thoughts I’d like to share. Plus I didn’t go through all the points of the comment from the very beginning. I know very well that the topic of programming paradigms and programming approaches can cause an interesting discussion, as this topic is loved on Hubre. I will be glad to hear the opinions of our dear readers!
Thank you all for your attention!
CITE THIS NOTEBOOK
CITE THIS NOTEBOOK
Objects: OOP in Wolfram Language
by Kirill Belov
Wolfram Community, STAFF PICKS, December 29, 2023
https://community.wolfram.com/groups/-/m/t/3092778
by Kirill Belov
Wolfram Community, STAFF PICKS, December 29, 2023
https://community.wolfram.com/groups/-/m/t/3092778