3  Understanding Wolframite

Where you learn a little more about how Wolframite works, so that you can use it effectively.

If you are in a hurry, you can just skim through this to know what answers you can find here when you need them. Though make sure to learn about the three types of expressions → Section 3.2 that Wolframite supports. We will refer to this chapter from other parts of the documentation.

First, we need some namespaces:

(ns understanding-wolframite
  (:require [scicloj.kindly.v4.api :as kindly]
            [scicloj.kindly.v4.kind :as kind]
            [scicloj.kindly.v4.kind :as k]
            [clojure.repl :as repl]
            [wolframite.core :as wl]
            [wolframite.lib.helpers :as h]
            [wolframite.wolfram :as w :refer :all
             :exclude [* + - -> / < <= = == > >= fn
                       Byte Character Integer Number Short String Thread]]
            wolframite.runtime.defaults))

Next, we need to actually start a Wolfram Kernel and connect to it:

(wl/start!)
{:status :ok, :wolfram-version 14.1}

3.1 How it all works

Wolframite works by translating Clojure data into Wolfram JLink Expr(essions) and sending them to an external, Wolframite-managed Wolfram process for evaluation, then translating the response back into data.

3.2 Expressions

Wolframite expressions consist of stuff that can be translated to Wolfram: data structures ([] for Lists and {} for Associations), constants, and Wolfram symbols (plus Wolframite aliases → Section 3.2.7). There are also some “Wolframite-isms” that we support, namely w/fn for defining ad-hoc functions, primarily for use with Map.

There are three ways of writing these expressions.

3.2.1 Evaluated form

The primary form you will encounter is the evaluated form, which uses vars from the wolframite.wolfram namespace as proxies for the actual Wolfram functions and symbols. It is the most convenient form, with support for code completion and mixing with evaluated Clojure code:

(wl/eval (w/+ (clojure.core/- 5 4) (w/Minus 1)))
0

When requiring wolframite.wolfram, we can “refer” most of its symbols, excluding those that conflict with Clojure or Java (such as +; have a look at the require at the top of this page), and thus we can also write:

(wl/eval (w/+ (- 5 4) (Minus 1)))
0

Notice that we have convenience vars for both Wolfram symbols and Wolframite aliases (see below) and thus both of the following work:

(=
  (wl/eval (w/+ 1 (w/- 1)))
  (wl/eval (Plus 1 (Minus 1))))
true

We can also mix Clojure and Wolfram - but the Clojure parts are evaluated on our side, before the rest of the expression is translated and sent to Wolfram.

The evaluated form is actually translated into the raw form (see Section 3.2.3) before being evaluated, as we can see if we run it on its own, without passing it to wl/eval:

(w/Plus (- 5 4) (w/Minus 1))
(Plus 1 (Minus 1))

You can see here that the convenience functions from the w/ namespace essentially evaluate to themselves in the symbolic, raw form - (w/Plus arguments...) becomes '(Plus arguments...).

3.2.2 A word on aliases

When we come back to our original expression, '(+ 1 (Minus 1)), you may notice that + is not actually a Wolfram function. It is a Wolframite alias, which we replace with Plus before we send it to Wolfram. You can read about it further down this document, in Section 3.2.7.

3.2.3 Raw form

The second form is the raw (quoted) data form, using actual Clojure symbols corresponding to Wolfram symbols or Wolframite aliases. This is what Wolframite uses internally.

Notice the ' quote in front of the expression, telling Clojure not to evaluate it but return it as-is:

(wl/eval '(+ 1 (Minus 1)))
0

We could also be more explicit and replace ' with the quote that it turns into under the hood:

(wl/eval (quote (+ 1 (Minus 1))))
0

However, quoting the whole form does not allow us to have any evaluations inside the expression. We can instead build the form manually, quoting only the symbols:

(wl/eval (list 'Plus 1 (list 'Minus 1)))
0

Notice that you will leverage quoting also with the evaluated form, namely when you create and refer to Wolfram-side variables, such as those created by (w/= 'myVar ...).

3.2.4 Advanced: Syntax quote

As mentioned, quoting a form makes it impossible to refer to any vars or evaluate any code within the form. We can bypass the problem by not quoting the whole form but replacing (...) with (list ...) and quoting each symbol. But there is yet another way, used by Clojure macros - namely the syntax quote `. It makes it possible to evaluate things within the quoted form by prefixing them with the “unquote” ~ (or “splicing unquote” ~@). The only problem here is that ` automatically fully qualifies all symbols, as we can see here:

(do `(Minus 42))
(wolframite.wolfram/Minus 42)

This would of course break our translation to Wolfram. This expression would be converted to wolframite.wolfram`Minus[42] and Wolfram knows no module called wolframite.wolfram. There is fortunately one trick to tell the Clojure Reader to keep a symbol unqualified by combining ~', essentially telling it “evaluate this expression, which returns a simple symbol”:

(do `(~'Minus 42))
(Minus 42)

3.2.5 Wolfram string form

There is one more form, the Wolfram string form, which is the raw Wolfram code in a string:

(wl/eval "Plus[1,Minus[1]]")
0

This is useful when you just want to paste in some Wolfram of when you need to use a feature that we don’t yet support.

3.2.6 Mixing different kinds of forms

The evaluated form may also contain sections in the other forms. You’d typically need that when the Wolframite evaluated form does not (yet) support that which you are trying to do.

When nesting a Wolfram string form, we need to explicitly tell Wolframite to treat it as an expression and not just as a primitive string, by passing it through wl/wolfram-expr:

(wl/eval (w/Plus
           '(Internal/StringToMReal "-1.5")
           (wl/wolfram-expr "Minus[3]")))
-4.5

3.2.7 Aside: Wolframite aliases

Aside of symbols that directly correspond to Wolfram symbols, you can also use Wolframite aliases. The aliases provide alternative names for Wolfram symbols. We have used above +, which is an alias for Plus. Here are all the built-in aliases that we currently support:

(k/table {:column-names [:Alias :Wolfram "Can be used without `w/` prefix?"],
          :row-vectors (-> wolframite.runtime.defaults/all-aliases
                           (dissoc '-)
                           (assoc '- "Minus or Subtract")
                           (->> (map (fn [[k v]] [k v (when-not (contains? recommended-exclusions k) "✅")]))))})
Alias Wolfram Can be used without `w/` prefix?
>_< Simplify
! Not
= Set
<*> Dot
< Less
=! Unset
|| Or
<= LessEqual
** Power
* Times
-> Rule
x> Replace
fn Function
do CompoundExpression
>>_<< FullSimplify
<<_>> ExpandAll
> Greater
_> RuleDelayed
<_> Expand
- Minus or Subtract
&& And
x>> ReplaceAll
=== SameQ
/ Divide
>= GreaterEqual
++<_> ComplexExpand
NonCommutativeMultiply
<> StringJoin
Integrate
+ Plus
+= AddTo
?? Information
== Equal
_= SetDelayed
-= SubtractFrom
Sqrt
++ Conjugate

Thus, the following two expressions are equivalent

(wl/eval '(+ 1 (- 1)))
0
(wl/eval '(Plus 1 (Minus 1)))
0

You can even add your own aliases, as discussed in Section 11.0.1.

3.2.8 Aside: Wolfram modules and fully qualified names

Most Wolfram functions are global, but they can also be placed inside modules and need to be referred to by their fully qualified names. While Wolfram uses ` to separate the module and the symbol, we write it as /. Thus, these two are equivalent:

(wl/eval "Internal`StringToMReal[\"-1.5\"]")
-1.5
(wl/eval '(Internal/StringToMReal "-1.5"))
-1.5

(Of course, you normally do not want to use the Internal/* functions, as they may disappear or change between Wolfram versions.)


3.3 Aside: Translating Wolfram to Wolframite

When asking the internets or ChatGPT how to do a certain thing in Wolfram, you will get a Wolfram answer. Thus you need to know how to translate it from Wolfram to Wolframite. Let’s say you have this Wolfram snippet:

Import["demo.csv.gz", {"Data", 1 ;; 3}, "HeaderLines" -> 1]

How do you turn it into Wolframite Clojure? Let’s ask for help!

(wl/->clj "Import[\"demo.csv.gz\", {\"Data\", 1 ;; 3}, \"HeaderLines\" -> 1]")
(Import "demo.csv.gz" ["Data" (Span 1 3)] (-> "HeaderLines" 1))

Now, we either need to quote that to turn it into the raw form, or rewrite it using the convenience functions of the evaluated form:

(wl/eval
 (Import "demo.csv.gz" ["Data" (Span 1 3)] (w/-> "HeaderLines" 1)))

3.4 Errors

We try to detect when Wolfram wasn’t able to evaluate your expression and throw an exception. However, sometimes we are not able to detect that. In such cases, the failure will often be indicated by the fact that you get the same, unevaluated expression back.

Here it works as designed:

(try (wl/eval (FromDigits "-87.6"))
     (catch Exception e
       (k/hiccup [:blockquote (ex-message e)])))
Evaluation seems to have failed. Result: FromDigits["-87.6"] Details: FromDigits::nlst: The expression -87.6 is not a list of digits or a string of valid digits.

Correct:

(wl/eval (FromDigits "87"))
87

3.5 Documentation

We can leverage Clojure repl’s documentation support with the Wolfram convenience vars:

(show-stdout
  (repl/doc GeoGraphics))
-------------------------
wolframite.wolfram/GeoGraphics
  GeoGraphics[primitives, options] represents a two-dimensional geographical image.

Search for symbols (case-insensitive):

(->> (repl/apropos #"(?i)geo")
     (drop 2)
     (take 3))
(wolframite.wolfram/$GeoLocationCountry
 wolframite.wolfram/$GeoLocationSource
 wolframite.wolfram/ArithmeticGeometricMean)

Search complete docstrings for a pattern:

(show-stdout
  (repl/find-doc "two-dimensional"))
-------------------------
wolframite.wolfram/Area
  Area[reg] gives the area of the two-dimensional region reg.
Area[{x1, …, xn}, {s, smin, smax}, {t, tmin, tmax}] gives the area of the parametrized surface whose Cartesian coordinates xi are functions of s and t.
Area[{x1, …, xn}, {s, smin, smax}, {t, tmin, tmax}, chart] interprets the xi as coordinates in the specified coordinate chart.
-------------------------
wolframite.wolfram/AstroGraphics
  AstroGraphics[primitives, options] represents a ...

If we evaluate (h/help! 'ArithmeticGeometricMean) then it will open the Wolfram documentation page for ArithmeticGeometricMean.

We could instead ask for the link(s):

(h/help! w/ArithmeticGeometricMean :links true)
["https://reference.wolfram.com/language/ref/ArithmeticGeometricMean.html"]

h/help! also works on whole expressions, providing docs for each symbol:

(h/help! '(GeoImage (Entity "City" ["NewYork" "NewYork" "UnitedStates"])) :links true)
("https://reference.wolfram.com/language/ref/GeoImage.html"
 "https://reference.wolfram.com/language/ref/Entity.html")

(Notice that help! works both with symbols and our convenience vars.)

source: notebooks/understanding_wolframite.clj