Group Abstract Group Abstract

Message Boards Message Boards

0
|
103 Views
|
10 Replies
|
2 Total Likes
View groups...
Share
Share this post:

A template for functions that produce 'object' functions

Hello! I find myself wanting to write functions that do a common WL thing: produce as their output an 'object' function which is then queried by sending arguments. The statistical fitting functions are an example: you get a 'fitted model' object from which you extract different outputs, e.g.,:

myFit["ParameterTable"]
myFit["AIC"]

But I can't seem to find any examples of how these 'object functions' are coded within the functions that produce them. I can think of some ways it could be done, but I'm guessing there is a standard way that WL functions do it, and I would prefer to just copy that.

Does anyone know? Is there a template?

POSTED BY: Gareth Russell
10 Replies
Posted 2 days ago

I usually use a pseudo-object-oriented paradigm in which an object has the form classHead[data]. Methods are defined for classHead as subvalues. I usually use an Association[] for data, but pre V10 this was not possible. Here are two variants. If I want to update data in my object (the mutable variant), then I need to be able to modify the association. To do this the argument has to be a symbol that refers to an association. In a package, I often create this symbol in a private subcontext, but below I do it manually in the "Global`" context. Further the argument must remain a symbol, so classHead[] must have an attribute like HoldAll. In the immutable variant, the data does not change, and the argument need not be held. In both variants, the methods can either return parts of the data or compute something from the data.

myObjImmutable // ClearAll;
myObjImmutable[data_Association]["Total"] := (* the "Total" method *)
  Total@myObjImmutable[data]["Data"];
myObjImmutable[data_Association][method_String] := (* generic data lookup *)
  Lookup[data, method, Message[myObjImmutable::notmethod]];

myObjMutable // ClearAll;
myObjMutable // Attributes = {HoldAll};
myObjMutable[data_Symbol]["Total"] /; AssociationQ@data :=  (* the "Total" method *)
  Lookup[data, "Total", data["Total"] = Total[data["Data"]]];
myObjMutable[data_Symbol][method_String] /; AssociationQ@data := (* generic data lookup *)
  Lookup[data, method, Message[myObjMutable::notmethod]];

my1 = myObjImmutable[<|"Data" -> Range[5]|>]
(*  myObjImmutable[<|"Data" -> {1, 2, 3, 4, 5}|>]  *)

my1["Total"]
(*  15  *)

my1 (* unchanged *)
(*  myObjImmutable[<|"Data" -> {1, 2, 3, 4, 5}|>]  *)

myData = <|"Data" -> Range[5]|>;
my2 = myObjMutable[myData]
(*  myObjMutable[myData]  *)

my2["Total"]
(*  15  *)

myData (* has been updated with "Total" *)
(*  <|"Data" -> {1, 2, 3, 4, 5}, "Total" -> 15|>  *)
POSTED BY: Updating Name

An Association with RuleDelayed will be evaluated only when requested:

myFit = <|"ParameterTable" :> Plot[Sin[x], {x, 0, Pi}],
  "AIC" -> somethingElse|>
POSTED BY: Gianluca Gorni

So the method employed by LinearModelFit is a hybrid: It does return a function, FittedModel, but this is an existing function defined externally (and hidden) and is returned with a bunch of info bundled into an Association as its argument:

FittedModel[<|"Type" -> "Linear", 
 "Model" -> <|"FittedParameters" -> {0.186441, 0.694915}, 
   "IndependentVariables" -> {x}, "BasisFunctions" -> {1, x}, 
   "LinearOffset" -> <|"Function" -> 0, "Values" -> 0|>|>, 
 "KeyNames" -> Automatic, "Weights" -> <|"ExampleWeights" -> 1.|>, 
 "InputData" -> {{0, 1}, {1, 0}, {3, 2}, {5, 4}}, 
 "UserDefinedDesignMatrixQ" -> False, 
 "DesignMatrix" -> {{1., 0.}, {1., 1.}, {1., 3.}, {1., 5.}}, 
 "Localizer" -> 
  Function[Null, FittedModels`LocalizedBlock[{x}, #1], {HoldAll}], 
 "Options" -> {}|>]

This bundled info is enough for FittedModel to calculate any of the requested outputs. So FittedModel must be defined using SubValues something like this:

FittedModel[params_]["PValue"]:=...
FittedModel[params_]["ParameterTable"]:=

So I think I am beginning to see how it works! Don't understand the "Localizer" association yet, but little steps...

POSTED BY: Gareth Russell
Posted 3 days ago

I don't know what you'd consider "standard", and I actually wouldn't assume that every such built-in "object" does it the same way. Nevertheless, a very idiomatic way to do this would be to define SubValues for your symbol.

POSTED BY: Eric Rimbey

Ah — I look up SubValues and see what you mean:

myDataObj[x_, y_]["Properties"] := {"Properties", "Sum", "Product"}
myDataObj[x_, y_]["Sum"] := x + y
myDataObj[x_, y_]["Product"] := x y

and in fact the use of myDataObj as a name strongly suggest this is the 'official' way! But these functions are defined as having inputs: you can't just call myDataObj["Sum"], you have call myDataObj[x,y]["Sum"]. So in the typical use case the necessary pieces need to be defined like this

myDataObj["Product"] := x y

Here is how I got it to work:

fittingFunction[x_, y_] := (fitObject["Sum"] := x + y; fitObject["Product"] := x*y; fitObject)
myFit = fittingFunction[2, 3];
myFit["Sum"] (* Yields 5 *)
myFit["Product"] (* Yields 6 *)

Can anyone from Wolfram confirm that this is the general framework?

I note that one can usually also do Normal[myFit]; still need to figure that out!

POSTED BY: Gareth Russell
Posted 3 days ago

you can't just call myDataObj["Sum"], you have call myDataObj[x,y]["Sum"]

Yes, but that's how it always works. The Wolfram Languages works on expressions. Everything is an expression.Evaluations rules are associated with symbols (or expressions involving said symbols), and the evaluation process churns through these until the result no longer changes.

Having said that, you certainly could call myDataObj["Sum"] if you provide that specific value in DownValues.

Just for curiosity, I looked at the FullForm for the result of a LinearModelFit. Just copying an example from the documentation:

data = {{0, 1}, {1, 0}, {3, 2}, {5, 4}};
lm = LinearModelFit[data, x, x];
FullForm[lm]
(* FittedModel[Association[...]] *)

So, at least in this specific case, Association is used as a data structure to hold the determinative parameters. But I assume that SubValues is used with definitions that depend on those parameters (since lm["Something"] wouldn't directly access the Association). Notice that one key in the association is "FittedParameters", but that you get an error message and no useful result from lm["FittedParameters"]. If you call lm["BestFitParameters"], then you get the value associated to "FittedParameters". So we have not only a level of indirection from nested structure, but also from a mapping between the valid input arguments and the valid keys inside the structure.

POSTED BY: Eric Rimbey

Yes, I discovered this myself; should have done so earlier. The difference compared to my suggested approach is the FittedModel function is independently defined, not within the fitting function. The LinearModelFit function essentially does a 'first step' of calculations, and then passes a set of results from that (not necessarily corresponding to the possible queries, as you point out) to the second function as an association. The second function is presumably mostly a big Which[] based on all the possible string arguments.

Having figured this out, v15 is introducing a new 'unified' model fitting framework, so before implementing anything myself I guess I should grab the beta and see if anything significant has changed in how this part is handled!

POSTED BY: Gareth Russell
Posted 3 days ago

I would not recommend your approach in your fittingFunction example. The main reason is that previous objects get destroyed every time you create a new object. I would suggest something simpler:

fittingFunction[x_, y_] := fitObject[x, y];
fitObject[x_, y_]["Sum"] := x + y;
fitObject[x_, y_]["Product"] := x*y

You can use Format to create a display version for fitObject if you want to hide the structure. Of course, the structure can be more complex than just a sequence of arguments. You can use an association as previously discussed or any expression or sequence of expressions you want. Having the head fitObject there gives you a wrapper/interface inside of which you can do whatever you want.

POSTED BY: Eric Rimbey

An association works about this way:

myFit = Association["ParameterTable" -> whatever, 
   "AIC" -> somethingElse];
myFit["ParameterTable"]
myFit["AIC"]
POSTED BY: Gianluca Gorni

Interesting. If that is how it is done, then that is simple. Does this imply that the different outputs ("whatever", "somethingElse") are always made up front? I get the impression that sometimes code is executed when requesting an output, for example to make a figure (perhaps so that the initial object is not too memory-hungry). I wonder if the right hand side of each association be an expression held unevaluated until requested.

POSTED BY: Gareth Russell
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard