11
$\begingroup$

Sometimes it is convenient to work in your package/app with "objects" like:

childObject = <|
  "id" -> 284,
  "text" -> "texttext",
  "property" -> <|
    "type" -> "String",
    "content" -> "test"
    |>
  |>

object = <|
  "id" -> 1,
  "name" -> "name1",
  "subobjects" -> {
    childObject
    }
 |>

But if you want to pass them around you have to be able to validate a structure of them easily etc etc. E.g. f[object_Association] is not enough.

Basically this question asks about neat implementation of basic "methods" neeeded for them. Which methods? I don't know yet but at least:

  • construction from inherited "class" + a few changed values
  • validating a minimal structure of an object (precise or not)
  • validating an exact structure
  • ...

I've started with validating a minimal structure:

childObjectPattern = KeyValuePattern@{
    "id" -> _Integer,
    "text" -> _String,
    "property" -> KeyValuePattern[{
       "type" -> _String,
       "content" -> _String
    }]
};

objectPattern = KeyValuePattern@{
    "id" -> _Integer,
    "name" -> _String,
    "subobjects" -> {childObjectPattern ...}
};

That's convenient, isn't it?

MatchQ[object, objectPattern]

True

But I need precision and I need to know where it doesn't match

childObject = <|  "id" -> 284,  "text" -> "texttext",
  "property" -> <|    "type" -> 1,    "content" -> "test"    |>
|>;

object = <|"id"->1, "name"->"name1", "subobjects"->{childObject}|>;

but MatchQ won't tell us that the "property" "type" of a childObject is incorrect. Here's what I'm currently doing:

ClearAll[match];

match[expr_, kvp_KeyValuePattern] := Merge[
   {expr, Association @@ kvp}, Apply[match]
];
match[
  expr : {___}, 
  {Thread[ Verbatim[Repeated | RepeatedNull], Alternatives][kvp_KeyValuePattern, ___]}
] := match[#, kvp] & /@ expr;

match[expr_, kvp : Except[_KeyValuePattern]] := MatchQ[expr, kvp]

match[object, objectPattern]
<|
    "id" -> True, 
    "name" -> True, 
    "subobjects" -> {
      <|
         "id" -> True, 
         "text" -> True, 
         "property" -> <|
           "type" -> False, 
           "content" -> True
         |>
      |>}
 |>
Position[%, False]
{{Key["subobjects"], 1, Key["property"], Key["type"]}}

Perfect!

In case where the initial set of questions is to broad:

  • Can match be improved/compactified?

  • Currently it doesn't care about keys that aren't present in patterns, but what should I do to make it checking exact structure. Replacing KeyValuePattern with Association will make them order dependent, which is not what I want.

The key is brevity.

$\endgroup$
2
  • $\begingroup$ "The key is brevity." ← What if you have a complex machinery that does the matching based on a simple pattern? $\endgroup$
    – Szabolcs
    Commented Jun 3, 2016 at 9:49
  • $\begingroup$ @Szabolcs The best would be to have a 10 lines of code I can just paste whenever I need it. But I think I can live with a bigger package as long as it isn't affecting so much the straightforward coding style presented here. I'm not sure if I'm clear :) $\endgroup$
    – Kuba
    Commented Jun 3, 2016 at 10:32

1 Answer 1

7
$\begingroup$

General

The real problem with objects is that sooner or later you really start to want some of the fields mutable. And once you want it, you really open a can of worms. Also, for methods, you will find very soon that you want some nice features such as pattern-based method overloading and the syntax similar to how we use functions in Mathematica. I do have a full-fledge object-oriented extension for M (yet another one!) in the works, and plan to publish it very soon. While it started brief, it is no longer.

In any case, I will try to provide an answer that would be close to the spirit of what you presumably are after.

Validation

Your match function looks pretty good. I will provide a slightly modified version below, but generally I don't see away to vastly improve it. Here is a modified version:

ClearAll[match, Matched];

$multiPattern = Thread[Verbatim[Repeated|RepeatedNull],Alternatives];

match[expr_,kvp_KeyValuePattern]:=
  Merge[{expr,Association @@ kvp},Apply[match]];

match[expr:{___},{$multiPattern[kvp_KeyValuePattern,___]}]:=
  match[#,kvp]& /@ expr;

match[expr_,kvp:Except[_KeyValuePattern]]:=
  Matched[MatchQ[expr,kvp]];

match[arg_]:=Matched[False];

Below is the function validate, which is the one I suggest to use, and which has an operator form. It is more precise than just match in two ways: it only looks for match failures in a form Matched[False] (thus excluding false negatives from free-hanging False in an expression), and it also disallows keys which are present in the pattern and not present in the object, and vice versa.

ClearAll[validate];
validate[patt_]:=Function[expr, validate[expr, patt]];

validate[expr_, patt_]:=
    FreeQ[match[expr, patt], Matched[False]];

Inheritance

I will try to answer the inheritance part, by providing a micro-framework that would add the support for it in what I think is a minimally intruding way.

Implementation

Here is the code:

ClearAll[DefType, DefMethod, Object, SuperType, ObjectQ, New, $types, TypeQ, Extends, MsgFail];

Object::nomethod = "Object type has no method `1`";
Object::invld = "Object `1` is not a valid object";

SetAttributes[MsgFail, HoldFirst];
MsgFail[mn_MessageName, args___]:= (Message[mn, args]; $Failed);

$types = <|Object -> True|>;

TypeQ[type_Symbol]:= KeyExistsQ[$types, type];

SuperType[Object] = Null;
SuperType[_] = Object;

ObjectQ[Object[assoc_Association]]:=ObjectQ[assoc];
ObjectQ[assoc_Association]:=
  KeyExistsQ[assoc, "Type"] && TypeQ[assoc["Type"]];
ObjectQ[_]=False;

GetType[Object[assoc_Association]?ObjectQ]:=assoc["Type"];
GetType[_]:=$Failed;

ObjectValidQ[o:Object[assoc_Association]?ObjectQ]:=
  Validator[GetType[o], KeyDrop[assoc,"Type"]];
ObjectValidQ[_]=False;

New[__]:=$Failed

SetAttributes[DefType, HoldFirst];
DefType[type_Symbol, validator_, clear_:True]:=
  Module[{},
    If[clear,ClearAll[type]];
    type /: Validator[type, assoc_Association]:=
      validator[assoc];
    type /: Validator[type, _]:=False;
    type /: New[type, assoc_Association]:= 
      With[{validated = validator[assoc]},
        Object[Append[assoc, "Type" -> type]] /; validated
      ];
    AppendTo[$types, type -> True];
    type
];

DefType[type_Symbol ~ Extends ~ super_Symbol?TypeQ, validator_]:=
  Module[{},
    ClearAll[type];
    type /: SuperType[type] = super;
    DefType[type, validator, False]
  ];    

DefMethod /: 
  SetDelayed[
    DefMethod[type_?TypeQ][method_Symbol[args___]],
    rhs_
  ]:=
    Module[{},
      If[DownValues[method] === {},
        method[Super, l___,o_Object?ObjectQ, r___ ]:=
          method[SuperType[GetType[o]], l, o, r];
        method[l___, o_Object?ObjectValidQ, r___] /; !MatchQ[{l},{Super | _?TypeQ,___}]:=
          method[GetType[o], l, o, r];
        method[___,o_Object,r___]/;!MatchQ[{l},{Super | _?TypeQ,___}]:=
          MsgFail[Object::invld, o];
        method[t_Symbol?TypeQ, argums___]:=
          method[SuperType[t], argums];
        method[Object,___]:=MsgFail[Object::nomethod, method]
      ];
      type /: method[type, args]:=rhs;
    ];

Usage

Creating an object

Let me illustrate how you can use it. First, we define a simple Person type:

DefType[Person, 
  validate[KeyValuePattern[{
    "FirstName" -> _String,
    "LastName" -> _String
  }
]]]

(* Person *)

Let us now create an object:

p = New[Person, <|"FirstName" -> "John", "LastName" -> "Smith"|>]
New[Person, <|"FirstName" -> "John", "SurName" -> "Smith"|>]

(*
   Object[<|"FirstName" -> "John", "LastName" -> "Smith", "Type" -> Person|>]

   $Failed
*)

The second attempt failed, since the object info did not pass validation. Note that the object construction is a simple process of validation plus adding a special field "Type". This field is reserved and should not be used as an info field.

Defining methods

Let us now define some methods:

ClearAll[getInfo, getLastName, getFirstName]
DefMethod[Person] @ getFirstName[_[assoc_]] := assoc["FirstName"];

DefMethod[Person] @ getLastName[_[assoc_]] :=assoc["LastName"];

DefMethod[Person] @ getInfo[o_] :=
  StringJoin["Name: ", getFirstName[o], "  ", getLastName[o]];

and use them:

getInfo[p]

(* "Name: John  Smith" *)

On an invalid object, the method will automatically fail:

getInfo[Object[ <|"FirstName" -> "John", "SurName" -> "Smith"|>]]
 During evaluation of In[674]:= Object::invld: Object Object[<|FirstName->John,SurName->Smith|>] is not a valid object
(* $Failed *)

Inheritance

Let us now create a subtype of Person:

DefType[Employee ~ Extends ~ Person, 
  validate[KeyValuePattern[{
    "FirstName" -> _String,
    "LastName" -> _String,
       "EmployeeInfo" -> KeyValuePattern[{
          "Company" -> _String,
          "Department" -> _String
       }]
    }]]
]

(* Employee *)

Now let us create an instance:

john = New[Employee,
  <|"FirstName" -> "John", "LastName" -> "Smith", 
    "EmployeeInfo" ->
      <|
        "Company" -> "Microsoft",
        "Department" -> "Sales"
      |>
  |>
]

(* Object[<|"FirstName" -> "John", "LastName" -> "Smith", "EmployeeInfo" -> <|"Company" -> "Microsoft", "Department" -> "Sales"|>, "Type" -> Employee|>] *)

We can still use the methods of the super-type:

getInfo[john]
getFirstName[john]

(* 
   "Name: John  Smith"
   "John"
*)

Suppose now that you want to redefine the getInfo method, so that it also tells us about the employment - related info for john:

DefMethod[Employee] @ getInfo[o : Object[assoc_]] :=
  StringJoin[
    getInfo[Super, o],
    "; Company: ", assoc["EmployeeInfo"]["Company"],
    "; Department: ", assoc["EmployeeInfo"]["Department"]
  ]

And now:

getInfo[john]

(* "Name: John  Smith; Company: Microsoft; Department: Sales" *)

As you can see, it works as we want, and also, you still have an access to the method of the supertype, by using the method[Super, args] form.

Summary

I presented a mini-framework that adds stronger typing and inheritance to immutable objects based on Association. It is really minimal, but hopefully still provides basic common tools, such as type-checking, object validation in method calls, inheritance and method overrides in subtypes. It can be further extended to incorporate other features.

I didn't have the time to thoroughly test the code, so it could contain a number of bugs (I am sure it does).

$\endgroup$
5
  • $\begingroup$ Don't bother answering if this was too long ago but maybe you recall what is the point of validate if one can just MatchQ[expr, patt]? $\endgroup$
    – Kuba
    Commented Feb 26, 2018 at 8:12
  • $\begingroup$ @Kuba A cursory look at the code above suggests that MatchQ might not be ideal, because KeyValuePattern is somewhat permissive. The match function above, performs stricter matching, and validate simply inspects the result of match for failures. Also, validate can easily be modified to provide a more detailed information about the parts that didn't match, which I didn't do here, but which was your original motivation for match. $\endgroup$ Commented Feb 26, 2018 at 19:28
  • $\begingroup$ @Kuba The funny thing is that I completely forgot about this answer, which actually looks quite useful also for some of the things I do. $\endgroup$ Commented Feb 26, 2018 at 19:29
  • $\begingroup$ Yes it does, and thanks for the comment. I need it for my lightweight error handling. I needed to modify $multiPattern because currently it matches {} with Repeated which should not happen. And since I'm on it I will probably add Matched[Missing] to the family. $\endgroup$
    – Kuba
    Commented Feb 26, 2018 at 19:32
  • $\begingroup$ @Kuba Sounds good. Feel free to edit my answer and add your changes. $\endgroup$ Commented Feb 26, 2018 at 20:27

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.