One of the things I like about Elixir is its support for patterns at the core language level, not through library functions as in most other languages. This contributes to writing cleaner code, in my opinion.
Another environment that I am familiar with, namely Mathematica, boasts of (arguably) the most powerful symbolic programming language called “Wolfram Language”. Mathematica is primarily used in mathematical and scientific programming. Patterns and Rules are all-pervasive in this language.
It is not fair to compare these two languages since each addresses a different kind of problem. Elixir is meant for highly scalable and fault tolerant general-purpose computing, whereas Mathematica excels in complex symbolic (and numeric) computations, and over the years has been extended to include Neural Networks, Machine Learning, Natural Language Understanding, and so on.
In this article, I am going to compare the pattern matching ideas supported by Elixir and Mathematica. Even this is a vast topic to address, so I will limit the discussion to the most striking aspects of pattern matching.
In the following sections, when I use the term “Mathematica”, I actually mean “Wolfram Language”.
The “=” Operator
Let us start with a simple idea. In Elixir, the operator “=” is called “Pattern Matching” operator, not an “Assignment Operator”, although the operation can involve assignment. Look at the following example:
Here, the two patterns will match only if X, Y, and Z are bound respectively to 1, 2 and 3. This causes the values to be bound to the variables. The operator, in this case, behaves like the assignment operator.
Here is a slight variation:
In this case, the first value of the LHS tuple is a constant and hence for the pattern to succeed, the RHS must have the same first element. The match succeeds since this is true, and Y and Z are bound as before.
What happens if there is a pattern mismatch? The following example shows this.
I hope you understand what is going on. There are other interesting variations of the pattern matching operator in Elixir, but we will not get into those here.
Coming to Mathematica, the “=” operator is the traditional “Assignment” operator. No pattern matching is occurs here. The following example is similar to what we tried in Elixir:
There is no surprise here since it merely involves assignment to the 3 variables on LHS. Now consider this:
Since we cannot assign to a constant on the LHS, the above fails. There is no attempt to “pattern match” in this case.
As the above examples show, the “=” operator in Elixir is more general than Mathematica’s assignment operator.
Case Statement
Elixir differs from most other programming languages in that it supports patterns (instead of simple conditions) in the “case” statement. Here is an example:
Notice how we use a “guard” along with the first pattern to check if the second element of the list is greater than 10. The second pattern matches any element in the second position of the list. The third pattern only checks if the expression is a list without considering its internal structure. The last condition will match anything that hasn’t matched the earlier three patterns. In this case, the given list “[1, 11, 3]” matches the first pattern.
Here is the same code as above but with the expression “[1, 9, 3]”. It matches the second pattern.
What is the result if we pass a 4-element list instead of a 3-element list? The third “qualified” pattern matches.
Finally, we pass a “string” instead of a list. In this case, the “catch all” pattern matches!
Mathematica uses “Switch” instead of “case”. Otherwise, the structure is quite similar. The following examples correspond to the above Elixir examples:
The operator “/;” puts constraints on the associated pattern. The following matches the second condition.
The following matches the third condition, a pattern that matches any “List”.
The last case corresponds to a “non-List” expression.
As you can observe, the behaviour is quite similar to Elixir.
Patterns and Guards in Functions
Both Elixir and Mathematica allow “guards” (or conditions) to be associated with function definitions. This is quite powerful. Let us go through an example to see how this is done.
Suppose that we want to compute the nth “Fibonacci” number. Here is one way to do this in Elixir:
Instead of coding the different trivial cases in a single function, we split the logic across multiple definitions. Notice also, how we are able to associate a “guard” or condition with the third definition to check if the passed argument is an integer (we take for granted that the argument is positive).
Here is yet another way. We have merged the first two definitions by introducing a “guard”.
It is important to keep in mind that Elixir tries the functions in the order of definition. So, if we change the order as in the following case, it will not work.
How do we write this function in Mathematica? The following implementation corresponds to the first version we wrote in Elixir.
Mathematica too allows us to use “conditions” as part of function definition. The code given below matches the second version we wrote in Elixir.
A nice thing about Mathematica is that there is no restriction on the order in which the definitions must be provided. So, even the following will work correctly.
Arbitrary Pattern-based Transformations in Mathematica
Unlike Elixir, Mathematica allows us to define functions that work with symbolic expressions. Take a look at the following snippet:
The first line above defines a function that takes two arguments. The first argument can be anything as denoted by the pattern “x_”. The second argument must be an expression of the form “x_ raised to the power of anything”. Given two arguments like this, this function applies another function “p” to 2 times “x” (remember “x” refers to the first argument).
The second and third lines show what happens when this function is called with “(m+n)” and “(m+n) 3”. The fourth and fifth lines show another valid invocation example. In both these cases, the argument structure is maintained and hence the expected transformation takes place. The last two lines show what happens when the second argument does not satisfy the required structure (it uses “n” instead of “m”).
As the final example of pattern transformations, Mathematica has a special “substitution” operator “//.” that can be applied to an arbitrary expression to transform it into another expression. Here is an example:
Here, the expression to the left of “//.” Is “X2 + Y2 + Z”. To the right of “//.” we have a list of substitutions to apply. The substitution operator repeatedly applies the given values to the LHS expression until it changes no more. Finally, we get the answer “16”.
Of course, the substitution can also be symbolic. Look at this:
Here we transform one symbolic expression to another. Such transformations are quite common in the domain of Mathematica.
In my view, Mathematica’s pattern matching capabilities are richer and more powerful than Elixir. What I have covered is just a tiny part of what Mathematica allows.
Hope you enjoyed reading this article. Have a nice weekend and a wonderful week ahead!
Recent Comments