4
$\begingroup$

I learned how to use OOP in Mathematica as shown by Leonid Shifrin (and others in this forum over the years).

It works well, until I moved the module that represents the object to inside a package, then I found all the public methods of the module no longer can be directly accessed as before and they all have to be made global at the package level now.

It is easier to explain all this by an example.

Using this small example by Leonid taken from this post which I modified very little

personClass[fname_, lname_] := Module[{fn = fname, ln = lname, self},
   SetAttributes[self, HoldAll];
   self@getFirstName[] := fn;
   self@setFirstName[name_] := fn = name;
   self@getLastName[] := ln;
   self@setLastName[name_] := ln = name;
   self@getFullName[] := fn <> " " <> ln;
   self];

Now in the notebook one can do a person object using the call

   obj = personClass["joe", "doe"]

After that, the object methods can be accessed using

   obj@getFullName[]

Mathematica graphics

This all works well. The problem comes when I wanted to put this object in a package. This is what I did

BeginPackage["mypkg`"] 
Unprotect @@ Names["mypkg`*"]; 
ClearAll @@ Names["mypkg`*"]; 
 
personClass::usage = "personClass[fname,lname]" 

Begin["`Private`"] 

personClass[fname_, lname_] :=
  Module[{fn = fname, ln = lname, self},
    SetAttributes[self, HoldAll];
    self@getFirstName[] := fn;
    self@setFirstName[name_] := fn = name;
    self@getLastName[] := ln;
    self@setLastName[name_] := ln = name;
    self@getFullName[] := fn <> " " <> ln;
    self
];

End[]; 
Protect @@ Names["mypkg`*"]; 
EndPackage[];

And saved the above in mypkg.m file. Everything looks OK so far. Except it is no longer possible to access those object methods directly as before. This is what happens in the notebook

 Get["mypkg.m"]
 obj = mypkg`personClass["joe", "doe"]

Mathematica graphics

Now when I do like before

 obj@getFullName[]

Mathematica does not see the object methods. It gives

 mypkg`Private`self$4172[getFullName[]]

None of the object methods are visible, like the case when the module was in the notebook before moving it to a package. I understand why that is (different context now), but I am looking for easier way to do this.

The solution I found is to modify the package and actually make all Person class methods global by adding an explicit usage statement for each public method in each Object module I want to put inside the package. So the package now looks like this

BeginPackage["mypkg`"] 
Unprotect @@ Names["mypkg`*"]; 
ClearAll @@ Names["mypkg`*"]; 
 
personClass::usage = "personClass[fname,lname]" 
getFullName::usage = "...." 
setFirstName::usage =  "...." 
getLastName::usage =  "...." 
setLastName::usage =  "...." 
getFullName::usage =  "...." 

Begin["`Private`"] 

personClass[fname_, lname_] :=
  Module[{fn = fname, ln = lname, self},
    SetAttributes[self, HoldAll];
    self@getFirstName[] := fn;
    self@setFirstName[name_] := fn = name;
    self@getLastName[] := ln;
    self@setLastName[name_] := ln = name;
    self@getFullName[] := fn <> " " <> ln;
    self
];

End[]; 
Protect @@ Names["mypkg`*"]; 
EndPackage[];

And now reloading the package with

Get["mypkg`"]

The calls now work as they did in the notebook outside the package scope

Mathematica graphics

But this defeats the whole point of using a module as a class for an object. If one have to manually retype the same name of each public method in each module with a usage statement like this. Also looking the usage statements, one has no idea from where each one of these methods came from. This makes managing this code a mess.

The question is: Is there a way to automate this, or better way to use objects inside packages than the above so I do not have to do this for each inner module inside the package like this? managing these usage statements will be a headache for very large modules.

May be there is a way to export all those public methods of the Person class automatically to the package level?


Update

Here is a simple example of the problem of using this method of making Objects in Mathematica. There are 2 classes (i.e. modules) in this example, each with a public method called process.

But since the name is the same, the usage for the first one is lost and only the second usage method shows up.

BeginPackage["mypkg`"] 
Unprotect @@ Names["mypkg`*"]; 
ClearAll @@ Names["mypkg`*"]; 
 
classA::usage = "classA[arg1,arg2]" 
process::usage = "classA`process[]" 

classB::usage = "classB[arg1]" 
process::usage = "classB`process[]" 

Begin["`Private`"] 

classA[arg1_, arg2_] :=
  Module[{a = arg1, b = arg2, self},
    SetAttributes[self, HoldAll];
    self@process[] := a+b;
    self
];

classB[arg1_] :=
  Module[{a = arg1, self},
    SetAttributes[self, HoldAll];
    self@process[] := a^2;
    self
];

End[]; 
Protect @@ Names["mypkg`*"]; 
EndPackage[];

Saving the above as mypkg.m and then doing

<<mypkg`
?mypkg`*

Mathematica graphics

?mypkg`process

Mathematica graphics

So the the first usage is lost, since it is the same name. This is just one of the problems with using this way of doing OOP that shows up when putting these modules inside a package.

Everything works well outside of the package ofcourse. But I needed to put these modules inside a package, since this is the standard way to distribute an application in Mathematica to others.

Note that in spite of the usage message getting lost (this is documentation problem), I am able to actually make objects of class A and class B OK and call the process method for each with no problem. Here is screen shot

Mathematica graphics

The above is all correct. So if there is a way to fix this usage problem, this setup could actually work. I just need to update the usage messages each time a new public method is added/changed for each member class.

$\endgroup$
3
  • 1
    $\begingroup$ It seems strings are used for method identifiers sometimes. For instance self@"getFirstName"[] := fn. $\endgroup$
    – Michael E2
    Jan 2, 2023 at 6:37
  • $\begingroup$ This may not be satisfactory: how about move those before Begin["Private"]; $\endgroup$
    – Lacia
    Jan 2, 2023 at 6:41
  • 1
    $\begingroup$ fyi, you don't need usages, getFullName; getLastName; ... is enough. $\endgroup$
    – Kuba
    Jan 2, 2023 at 7:34

2 Answers 2

1
$\begingroup$

To my understanding, your problem is about how Mathematica assign and finds contexts for variables. In normal operation, they are all in the Global, so they can be called and searched easily but by introducing the package and Private, symbols even the function arguments pick those contexts, so when you call the obj @ getFullName[] there is no Global'getFullName or mypkg'getFullName but a mypkg'Private'getFullName which work fines if you mention it specifically:

obj = mypkg`personClass["joe", "doe"]
(* Out: mypkg`Private`self$4531 *)

obj @ mypkg`Private`getFullName[]
(* Out: "joe doe" *)

One possible solution you found is to introduce these methods before Private. Another is declaring their context, so they don't automatically pick Private:

...
self @ mypkg`getFullName[] := fn <> " " <> ln;
...

And later:

obj @ getFullName[]

(* Out: "joe doe" *)
$\endgroup$
3
  • $\begingroup$ That is a nice trick. Yes, it eliminate having to type in the usage messages for each method in the classes. I still have to document those somewhere else, but that is ok. $\endgroup$
    – Nasser
    Jan 3, 2023 at 10:33
  • $\begingroup$ @Nasser As Kuba mentioned, you don't need to write the usage message, just introduce them to Mathematica, so they pick the package context not Private. A symbol; is sufficient if you want to go that route. $\endgroup$
    – Ben Izd
    Jan 3, 2023 at 10:38
  • 1
    $\begingroup$ I knew that I can just put the name without the rest of the usage message. But I still do not want to do that, as it becomes too messy to keep adding/deleting names as I change/modify the modules inside. I would have to remember to keep things in sync all the time. $\endgroup$
    – Nasser
    Jan 3, 2023 at 10:44
1
$\begingroup$

Thoughts/suggestions masquerading as an answer...

Given this particular approach to OO in MMA, I would say that having to declare the method names is actually a "feature" rather than a "headache", no matter how arduous it seems.

But, rather than let the symbol representing the method get resolved to its full context, you might be able to add a layer of indirection. Maybe capture the SymbolName and then lookup the private version of the function from some table. (I haven't tried this yet.)

What motivated this approach? It's kind of interesting to view an "object" as a function, but this approach seems awkward. It seems that the only advantange here is that it sort-a kind-a looks like the dot style that most OO languages use. I would focus more on the mechanics and ditch the notational trickery.

Why would one not use Associations to implement OO? That seems to me to be the most straightforward way, and it's also surprisingly close to how OO is often done anyway. Once you have a clean implementation of the "hidden" layer, then you can come up with a notational convention, and I think it'll be easier to avoid the headaches you're experiencing.

$\endgroup$
7
  • $\begingroup$ Thanks for the thoughts. having to put each name of a method in the global part of a package is a problem. What if I have 2 objects with methods the same name? But besides this, I did not use Association before to make objects. I first learned this method as I said from the links given, and it seemed like a nice method that is very close to how other OO work in other languages, this is until I wanted to put this class inside a package where these problems showed up. I looked at using Associations for objects, but I found the syntax not so easy to follow as in the above. I have to revisit that. $\endgroup$
    – Nasser
    Jan 2, 2023 at 20:21
  • $\begingroup$ What if I have 2 objects with methods the same name?. That's kind of the whole point. By using this approach, you're inviting name conflicts on the methods. At least by making them public, you won't hide the conflicts when they occur. $\endgroup$
    – lericr
    Jan 2, 2023 at 20:34
  • $\begingroup$ Actually, now that I think about it, having name conflicts on the methods isn't a problem, because you've created DownValues for the instance variable itself. Also, if you don't "publish" the methods, how are users supposed to know which methods your class offers? $\endgroup$
    – lericr
    Jan 2, 2023 at 22:31
  • $\begingroup$ if you don't "publish" the methods, how are users supposed to know which methods your class offers? This will be part of the documentation. Just like when one write a class in say C++, the documentation will tell the users what are the public methods. i.e. describe the API to the class. $\endgroup$
    – Nasser
    Jan 2, 2023 at 23:10
  • $\begingroup$ As a consequence of putting your usage messages right after the BeginPackage you get both "publishing" and documentation. I think this is pretty standard. Were you going to do documentation differently? $\endgroup$
    – lericr
    Jan 2, 2023 at 23:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.