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).