39
$\begingroup$

In a program I'm writing, I create a list of lists, that looks something like this:

{{100, {1,2,3,4,5}},
 {105, {2,4,6,8}},
 {42, {42,39,56}}}

I then pass this list of lists around to other functions. All well and good -- the problem is that this program has been developed iteratively, and this representation has changed, and so now I'd like to take more care in checking that the function is receiving the right kind of input.

In another language I could make this list of lists into its own type, and the compiler would enforce things. I know Mathematica has some facility for doing type checking, at least with primitive types, such as:

myFunc[a_String, b_Integer] := ...

But are there facilities available for creating (and enforcing, or at least checking) one's own ADTs?

$\endgroup$
1

4 Answers 4

37
$\begingroup$

In practice, enforcing strong types in Mathematica seldom pays off, just because, as mentioned by @belisarius, Mathematica is untyped (and perhaps more so than most other langauges, since it is really a term-rewriting system). So, most of the time, the suggestion of @Mr.Wizard describes what I'd also do.

The way to define ADT-s (strong types) was described in depth by Roman Maeder, in his books on Mathematica programming. This requires something more than what you provided in your question - namely, a more formal definition of what is in your data structure (so that we can form constructors, selectors and mutators). I will give here a very simple example to show how ADT can be implemented in Mathematica. The key points are using UpValues and (mostly inert) symbols to serve as heads of new types. Consider a simple "pair" type:

ClearAll[pair];
pair /: getFirst[pair[fst_, sec_]] := fst;
pair /: setFirst[pair[_, sec_], fst_] := pair[fst, sec];
pair /: getSecond[pair[fst_, sec_]] := sec;
pair /: setSecond[pair[fst_, _], sec_] := pair[fst, sec];

We can now define some function on this new type:

Clear[sortPairsByFirstElement];
sortPairsByFirstElement[pairs : {__pair}, f_] :=
     Sort[pairs, f[getFirst[#1], getFirst[#2]] &];

And here is an example of use:

pairs = Table[pair[RandomInteger[10],RandomInteger[10]],{10}]

{pair[0,10],pair[4,7],pair[5,3],pair[10,9],pair[9,2],pair[6,10],pair[3,7], pair[4,2],pair[0,4],pair[3,9]}

 sortPairsByFirstElement[pairs,Less]

{pair[0,4],pair[0,10],pair[3,9],pair[3,7],pair[4,2],pair[4,7],pair[5,3], pair[6,10],pair[9,2],pair[10,9]}

You can enforce stronger typing on what can go into a pair. One thing I've done is to enforce that in the "constructor":

pair[args__] /; ! MatchQ[{args}, {_Integer, _Integer}] :=
    Throw[$Failed, pair];

The technique just described produces truly strong types, in contrast to the pattern-based typing. Both are useful and complementary to each other. One reason why such strong typing as described above is rarely used in Mathematica is that all the rest of the infrastructure usual for the strongly-typed languages (compiler, type system, smart IDE-s, type-inference) is missing here (so you'd need to construct that yourself), plus often this will induce at least some overhead. For example, we may wish to represent an array of pairs as a 2-dimensional packed array for efficiency, but here the pair type will get in the way, and we'd have to write extra conversion functions (which will induce an overhead, not to mention the memory-efficiency). This is not to discourage this type of things, but just to note that over-using them, you may lose some advantages that Mathematica offers.

$\endgroup$
5
  • $\begingroup$ Is there a performance advantage of using UpValues instead of DownValues for functions that operate exclusively on the type (e.g. your getFirst method)? I could imagine this being so, because UpValues are tried first anyways, so why not use them. As an aside, I think it also helps performance a bit to give heads for custom data structures the HoldAllComplete attribute so that they are always perfectly untouched by evaluation. $\endgroup$
    – masterxilo
    Sep 19, 2016 at 12:44
  • $\begingroup$ @masterxilo I think that performance advantage is minimal, if at all. In practice, UpValues solve a different problem. If you have more than one type using the same methods, UpValues make sure that each type redefines those methods separately, and does not interfere with the other type. I discussed this more here. As to HoldAllComplete - again, while there may be performance advantage in doing this, cases where this really matters are only a few, in my experience (e.g. linked list type). $\endgroup$ Sep 19, 2016 at 13:06
  • $\begingroup$ @masterxilo There are, however, valid cases to assign the wrapper type some of the Hold* - attributes (can be HoldAll, not necessarily HoldAllComplete) - such as when you want to create mutable data structures and then those wrappers hold the state. One simple example of this approach I discussed here. Either way, given the top-level nature of this typing scheme, it is usually picked not to improve the performance but to improve the readability, robustness and code organization. If you need performance, use packed arrays / vectorization $\endgroup$ Sep 19, 2016 at 13:08
  • $\begingroup$ @LeonidShifrin I understand how 'UpValues' enable local overloading of protected functions and why is that better than unprotecting, but I can't see any advantage in this context. How does 'pair /: getFirst[pair[fst_, sec_]] := fst;' makes 'pair' stronger type than just having 'pair[,]' pattern and using that pattern directly to define other functions, such as 'sortPairsByFirstElement' in your example? $\endgroup$
    – Ivica M.
    Dec 1, 2016 at 17:00
  • 1
    $\begingroup$ @IvicaM. It doesn't. The strong type is defined by the pair head and the corresponding pattern (pair[_,_] or _pair), as you correctly noted. UpValues don't make the type "stronger", they make overloading softer and more local. In this example, I could instead of UpValues, just use DownValues like getFirst[pair[fst_, sec_]] := fst; etc. The difference would show up if we wanted to reuse getFirst and other method symbols for several different types - in this case, UpValues would provide a cleaner mechanism for overloading them. In the case at hand, their use is not mandatory. $\endgroup$ Dec 1, 2016 at 20:32
26
$\begingroup$

As you mentioned in your question and belisarius illustrates above, you can check arguments with arbitrary pattern matching.

When I need to do checks of this kind I often use a couple of methods; I will define the pattern once and then reference it by name:

p1 = {{_Integer, {_Integer ...}} ...};

dat = {{100, {1, 2, 3, 4, 5}}, {105, {2, 4, 6, 8}}, {42, {42, 39, 56}}};

f[x : p1] := First[x]

f[dat]
{100, {1, 2, 3, 4, 5}}

I also will make this check only once in a Module that calls other functions so that this check is not wastefully made a number of times. This may be common sense but I mention it anyway.

$\endgroup$
1
  • 3
    $\begingroup$ This is exactly what I was going to suggest. Damned sleep interrupting my attempts to garner reputation. :) $\endgroup$
    – rcollyer
    Apr 23, 2012 at 12:25
21
$\begingroup$

You can effectively create your own types by using the feature that Mathematica expressions have a Head, the head can be used to define a type. Functions can then use the Head value to apply only to arguments matching the defined type.

A version with loose format checking, format checked only upon creation,can be implemented as simply as this:

(* Define your type *)
    ValidMyTypeQ[data_List] :=(* check data format here *) 

    CreateMyType[data_List] := If[ValidMyTypeQ@data, MyType @@ data,Print@"Incorrect format"]

(* Utilise your type *)
    myFunction[data_MyType] := (* do your funky stuff *)

A stiffer version could format check each time MyType is passed to a function:

(* Define your type *)
 ValidMyTypeQ[data_MyType] :=(* check data format here *)  

 CreateMyType[data_List] := If[ValidMyTypeQ[MyType@@data], MyType@@data,
                            Print@"Incorrect format"]

(* Utilise your type *)    
    myFunction[data_MyType?ValidMyTypeQ] := (* do your funky stuff *)
$\endgroup$
3
  • $\begingroup$ I was remiss not to mention PatternTest which I also use when appropriate (see this for explanation). +1 $\endgroup$
    – Mr.Wizard
    Apr 23, 2012 at 11:54
  • 2
    $\begingroup$ +1 A practical example of this approach, showing how simple and readable it is, appears in a reply at mathematica.stackexchange.com/questions/1900 (the confetto type and associated methods). The reply by @Leonid Shifrin shows what is involved in a complete implementation. How far you go down this road depends on how complicated the data structures are and how reliable you need the code to be. $\endgroup$
    – whuber
    Apr 23, 2012 at 15:03
  • $\begingroup$ @whuber thanks, very elegant example both visually and software design wise $\endgroup$ Apr 23, 2012 at 16:41
19
$\begingroup$

No ADT in Mma (natively at least) ... but in your case you could use pattern matching:

yours = {{100, {1, 2, 3, 4, 5}}, {105, {2, 4, 6, 8}}, {42, {42, 39, 56}}}; 
f[x_] := 1 /; MatchQ[x, List[List[_Integer, List[_Integer ...]] ...]]
f[yours]
f["mySymbol"]

(*->
1
f["mySymbol"]
*)
$\endgroup$
8
  • $\begingroup$ +1 -- any reason for the explicit long form List pattern? Given that not everyone is familiar with FullForm this seems to potentially obscure your answer rather than strengthen it, unless you are also making some point about FullForm that I'm missing. $\endgroup$
    – Mr.Wizard
    Apr 23, 2012 at 6:21
  • $\begingroup$ @Mr.Wizard I am sure you know that no everyone matches the pattern "obscurity" with the same expressions. In particular, when I match some Heads (List being one of them), I prefer to see them explicitly in the pattern. Just taste. $\endgroup$ Apr 23, 2012 at 11:38
  • 1
    $\begingroup$ @Mr.Wizard For example, when my match comes from inspecting a FullForm[], I prefer to keep the FullForm output format, with explicit heads. $\endgroup$ Apr 23, 2012 at 12:07
  • 1
    $\begingroup$ Is there a reason why you used MatchQ instead of using the pattern directly, as in f[x:{{_Integer,{___Integer}} ...}] := 1? $\endgroup$
    – celtschk
    Apr 23, 2012 at 14:32
  • 1
    $\begingroup$ However you hopefully are aware that there are subtle differences. For example, f1[x_Integer,y_]:=1;f1[x_Integer,y_Integer]:=2;f1[1,2] gives 2 while f2[x_,y_]/;MatchQ[x,_Integer]:=1;f2[x_,y_]/;MatchQ[{x,y},{_Integer,_Integer}]:=2;f2[1,2] gives 1. $\endgroup$
    – celtschk
    Apr 23, 2012 at 17:30

Your Answer

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

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