General
I will show two (similar) ways of doing something like what you requested. Both are based on reusing the TypeSystem`
package, which is built in V10+. This package has several predefined types - inert symbols to denote them, actually. In particular, it has Atom
, Vector
, Tuple
, Assoc
and Struct
, with Atom
and Vector
immediately relevant for us.
Using TypeSystem`
We need to load the package first:
<<TypeSystem`
Now, we can do a few things. For example, this would be our test data:
dt = {{"a", "b", "c"}, {"d", "e", "f"}}
dtragged = {{"a", "b", "c"}, {"d", "e"}}
We can use the function DeduceType
, to deduce the types of these:
t = DeduceType[dt]
tr = DeduceType[dtragged]
(*
Vector[Vector[Atom[String], 3], 2]
Vector[Vector[Atom[String], AnyLength], 2]
*)
We can also generate the type-validation function, using DataValidationFunction
, like so:
vf = DataValidationFunction[t]
(* Length[#1] == 2 && MatrixQ[#1, StringQ[#1] || GeneralUtilities`MissingQ[#1] &] && Length[#1[[1]]] == 3 & *)
or
vfr = DataValidationFunction[tr]
(* ListQ[#1] && Length[#1] == 2 && VectorQ[#1, VectorQ[#1, StringQ[#1] || GeneralUtilities`MissingQ[#1] &] &] & *)
There are many other things one can do, but these are sufficient for our purposes.
Custom assignment operator using TypeSystem
We can now define a custom assignment operator, that would preprocess your definitions:
ClearAll[defTS];
SetAttributes[defTS, HoldAll];
defTS[(h : (Set | SetDelayed))[lhs_, rhs_]] :=
h @@ Append[
Hold[lhs] /. t : _Vector | _Atom :>
With[{vf = DataValidationFunction[t]}, (_?vf) /; True],
Unevaluated[rhs]
]
It uses the Trott - Strzebonski technique to inject into held code. Here is an example:
defTS[ff[a : Vector[Vector[Atom[String], AnyLength], 2]] := Length[a]]
The resulting definition is then:
?ff
Global`ff
ff[a:_?(ListQ[#1]&&Length[#1]==2&&VectorQ[#1,VectorQ[#1,StringQ[#1]||GeneralUtilities`MissingQ[#1]&]&]&)]:=Length[a]
Compiling to patterns
We could also, instead of using the DataValidationFunction
, reuse just the syntactic part of what TypeSystem`
provides, and "compile" that to patterns, which are familiar and idiomatic way to test arguments. Here is a simple example:
ClearAll[compile];
compile[Atom[atomType_]] := _atomType;
compile[Vector[Atom[atomType_], AnyLength]] := {___atomType};
compile[Vector[type_, AnyLength]] := {compile[type] ...};
compile[Vector[type_, length_Integer]] := {Repeated[compile[type], length]};
For example:
compile[t]
compile[tr]
(*
{Repeated[{Repeated[_String, 3]}, 2]}
{Repeated[{___String}, 2]}
*)
Now, we can do something similar to what we did before: define a custom assignment operator:
ClearAll[def];
SetAttributes[def, HoldAll];
def[(h : (Set | SetDelayed))[lhs_, rhs_]] :=
h @@ Append[
Hold[lhs] /. t : _Vector | _Atom :> RuleCondition[compile[t]],
Unevaluated[rhs]
]
where I use a shorter form of Trott - Strzebonski technique, which uses RuleCondition
. Now, if we use this one,
def[f[a : Vector[Vector[Atom[String], AnyLength], 2]] := Length[a]]
we get the following definition generated:
?f
Global`f
f[a:{Repeated[{___String},2]}]:=Length[a]
One can extend compile
to handle more cases. As shown, it is very simplistic, just to illustrate the idea.
At this point, however, we may ask, whether it was worth the trouble to start with the TypeSystem`
, if all we want is to define argument tests for a function, using just usual patterns. The conciseness of patterns is probably one reason why people generally don't bother with formal generics / template system just for the purposes of defining functions - in any particular case it is usually much simpler to just write the relevant patterns.