Skip to content

ADR: Use interned Wolfram symbols to construct forms and wl eval to evaluate them

Jakub Holý edited this page Feb 4, 2024 · 2 revisions

  • status: accepted
  • date: 2024-002-04
  • deciders: Jakub Holy, Thomas Clark

Context and Problem Statement

We want writing Wolfram expressions in Clojure to feel natural and be productive, but also understandable. Wolfram is a huge language - being discoverable, with integrated docs is important for developer experience.

Currently, we can evaluate string Wolfram expressions, Wolfram expressions written in terms on Clojure data with symbols, and vars for these symbols.

Decision Drivers

  • Seamless integration into Clojure dev experience
  • Predictable, understandable behavior
  • Efficiency

Considered Options

Wolfram expressions as symbols

Ex.: '(Plus 1 2); => Plus[1,2]

Pros: This works out of the box

Cons: No help from the IDE. Including variables is cumbersome - we need to switch to backtick and unquote+quote to avoid namespaced symbols:

(def two 2)
(wl/eval `(~'Plus 1 ~two))

Wolfram expressions using vars, where functions automatically evaluate themselves via Wolfram Kernel

Ideally, we would like to have Wolfram functions that behave like first class citizens in Clojure and can simply be called, just like Clojure functions:

(is (= 2 (Plus 1 1)))

This has been implemented by interning Wolfram symbols as Clojure vars where the value is a "proxy function", which executes by calling Wolfram with its name and arguments. But there are two problems with this:

  1. It doesn't work for non-fuction symbols such as Pi (e.x.: (Plus 1 Pi), or functions used as arguments (in Clojure terms, something like inc in (map inc ...); not sure if this is ever done in Wolfram). This is because these wouldn't be symbols that Wolfram can understand, but anonymous Clojure functions. Given the nature of Wolfram, there is no clear distinction between a function and non-functions, as we have in Clojure, so trying to make them behave differently is impossible.
  2. Nested function calls would make corresponding number of Wolfram invocations instead of just one, which is inefficient. E.g. (Plus 1 (Plus 2 3)) would call Wolfram fist for 2+3 then for 1+5.

Wolfram expressions using vars, with explicit evaluation

Do intern all Wolfram language symbols as Clojure vars, with docs, but these evaluate into symbols. The resulting symbolic expression is evaluated explicitely by passing it to wl/eval.

Pros: Understandable, user has control over and awarness of the evaluation. Docs are provided, the symbols are discoverable via clojure.repl/apropos etc. Some editors do support autocompletion (e.g. VS Code with Calva).

Cons: Not so seamless, as a manual call to wl/eval is necessary. Also, dynamically created vars like these do not work with all tools, especially those that rely on static code analysis, such as Cursive and clj-kondo.

Decision Outcome

Chosen option: Wolfram expressions using vars, with explicit evaluation, because of the best mixture of good development experience and control