Skip to content
This repository has been archived by the owner on Oct 19, 2022. It is now read-only.

A working implementation #21

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
22 changes: 22 additions & 0 deletions Manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This file is machine-generated - editing it directly is not advised

[[IterTools]]
git-tree-sha1 = "05110a2ab1fc5f932622ffea2a003221f4782c18"
uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
version = "1.3.0"

[[Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"

[[MathLink]]
deps = ["Libdl", "Printf"]
git-tree-sha1 = "046a0877b2d230c860d780e3bd9129bb4cb5dc66"
uuid = "18c93696-a329-5786-9845-8443133fa0b4"
version = "0.1.0"

[[Printf]]
deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
14 changes: 14 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name = "Mathematica"
uuid = "f648be94-fd14-429f-aa16-12a8ae75336b"
authors = ["Peng Guanwen <[email protected]>"]
version = "0.1.0"

[deps]
IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
MathLink = "18c93696-a329-5786-9845-8443133fa0b4"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
145 changes: 58 additions & 87 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,119 +2,90 @@

[![Gitter chat](https://badges.gitter.im/one-more-minute/Mathematica.jl.png)](https://gitter.im/one-more-minute/Mathematica.jl)

The `Mathematica.jl` package provides an interface for using [Wolfram Mathematica™](http://www.wolfram.com/mathematica/) from the [Julia language](http://julialang.org). You cannot use `Mathematica.jl` without having purchased and installed a copy of Mathematica™ from [Wolfram Research](http://www.wolfram.com/). This package is available free of charge and in no way replaces or alters any functionality of Wolfram's Mathematica product.
The `Mathematica.jl` package provides an interface for using [Wolfram Mathematica™](http://www.wolfram.com/mathematica/) from the [Julia language](http://julialang.org). You cannot use `Mathematica.jl` without having purchased and installed a copy of Mathematica™ from [Wolfram Research](http://www.wolfram.com/). Alternatively, you can install [Free Wolfram Engine for Developers](https://www.wolfram.com/engine). This package is available free of charge and in no way replaces or alters any functionality of Wolfram's Mathematica product.

The package provides is a no-hassle Julia interface to Mathematica. It aims to follow Julia's philosophy of combining high-level expressiveness without sacrificing low-level optimisation.
The package provides is a no-hassle Julia interface to Mathematica.

```julia
Pkg.add("Mathematica")
````
Provided Mathematica is installed, its usage is as simple as:
Provided Mathematica is installed, You can easily build a Mathematica expression in Julia and use `weval` to evaluate it in Mathematica, and get the converted Julia result.

```julia
using Mathematica
Fibonacci(1000)
#=> 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875
julia> using Mathematica
julia> weval(:Fibonacci(1000))
43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875
```
All of Mathematica's functions are available as both functions and macros, and splicing (`$`) works as you would expect:
```julia
Integrate(:(x^2), :x) # or
@Integrate(x^2, x)
#=> :(*(1//3,^(x,3)))

@Integrate(log(x), {x,0,2})
#=> :(+(-2,log(4)))

eval(ans) # or
@N($ans) # or
N(ans) # or
@N(Integrate(log(x), {x,0,2}))
#=> -0.6137056388801094
```
Including those that return Mathematica data:
All Julia symbols are automatically converted to Mathematica symbols.

```julia
@Plot(x^2, {x,0,2})
#=> Graphics[{{{},{},{Hue[0.67, 0.6, 0.6],Line[{{4.081632653061224e-8,1.6659725114535607e-15},...}]}}}, {:AspectRatio->Power[:GoldenRatio, -1],:Axes->true, ...}]
julia> weval(:Integrate(:x^2, :x))
W"Times"(W"Rational"(1, 3), W"Power"(W"x", 3))

julia> weval(:Integrate(:Log(:x), [:x,0,2]))
W"Plus"(-2, W"Log"(4))

julia> weval(:N(ans))
-0.6137056388801094
```
Mathematical data can participate in Julia functions directly, with no wrapping required. For example -

Some functions like `N` are often followed by `weval`. As a convenient method, `@importsymbol` can be used to reduce use of `weval`.

```julia
using MathLink
d = BinomialDistribution(10,0.2) #=> BinomialDistribution[10, 0.2]
probability(b::MExpr{:BinomialDistribution}) = b.args[2]
probability(d) #=> 0.2
julia> @importsymbol N, Integrate

julia> Integrate(:Log(:x), [:x, 0, 2])
W"Plus"(-2, W"Log"(4))

julia> N(ans)
-0.6137056388801094
```

Julia compatible data (e.g. lists, complex numbers etc.) will all be converted automatically, and you can extend the conversion to other types.
Returned Mathematica data can be displayed as SVG or LaTeX, in supported frontend like Juno and IJulia:

Note that Mathematica expressions are *not* converted to Julia expressions by default. Functions/macros with the `::Expr` hint (see below) will convert their result, but for others you must use `convert` or `MathLink.to_expr`.
![screenshot](./img/display.png)

Julia compatible data (e.g. lists, complex numbers etc.) will all be converted automatically, and you can extend the conversion to other types.

```julia
Log(-1) #=> Times[0 + 1im, :Pi]
convert(Expr, ans) #=> :(*(0 + 1im,Pi))
N(Log(-1)) #=> 0.0 + 3.141592653589793im
```
Printing and warnings are also supported:
```julia
Print("hi")
#=> hi
@Print(x^2/3)
#=> 2
# x
# --
# 3
Binomial(10)
#=> WARNING: Binomial::argr: Binomial called with 1 argument; 2 arguments are expected.
#=> Binomial[10]
```
Finally, of course:
```julia
WolframAlpha("hi") #=>
2-element Array{Any,1}:
{{"Input",1},"Plaintext"}->"Hello."
{{"Result",1},"Plaintext"}->"Hello, human."
```
julia> weval(:WolframAlpha("hi")) # =>
2×2 Array{Any,2}:
Any[Any["Input", 1], "Plaintext"] "Hello."
Any[Any["Result", 1], "Plaintext"] "Hello, human."

## Advanced Use
### Typing
In the file `Mathematica.jl`, you'll see a listing of function and macro specifications, each in one of these formats:
```julia
Function::ReturnType # or
Function(Arg1Type, Arg2Type, ...)::ReturnType # (functions only)
```
For example:
```julia
Integrate::Expr
RandomReal(Number)::Float64
RandomReal(Number, Integer)::Vector{Float64}
```
The return type hint here is an optimisation; it allows `MathLink.jl` to grab the value from Mathematica without first doing a type check, and makes the function type stable - for example, `RandomReal(10, 5)` would return an `Any` array if not for this definition. The argument types allow type checking and multiple definitions.

Not many functions have type signatures yet, so providing them for the functions you want to use is an easy way to contribute.

### Extending to custom datatypes
## Extending to custom datatypes

The Mathematica data expression `Head[x,y,z,...]` is represented in Julia as `MExpr{:Head}(args = {x,y,z,...})`. We can extend `Mathematica.jl` to support custom types by overloading `MathLink.to_mma` and `MathLink.from_mma`.
The Mathematica data expression `Head[x,y,z,...]` is represented in Julia as `WExpr(WSymbol(:Head), [x,y,z,...])`. To convert Julia types to `WExpr`, `Mathematica.buildexpr` should be implemented:

For example, we can pass a Julia Dict straight through Mathematica with just two lines of definitions:
```julia
using MathLink; import MathLink: to_mma, from_mma
d = [:a => 1, :b => 2]
import Mathematica: buildexpr, WSymbol

to_mma(d::Dict) = MExpr{:Dict}(map(x->MExpr(:Rule, x[1], x[2]),d))
Identity(d) #=> Dict[:b->2, :a->1]
from_mma(d::MExpr{:Dict}) = Dict(map(x->x.args[1], d.args), map(x->x.args[2], d.args))
Identity(d) #=> {:b=>2,:a=>1}
```
struct Less
lhs
rhs
end

## Usage Issues
buildexpr(s::Less) = WSymbol("Less")(buildexpr(s.lhs), buildexpr(s.rhs))
```
To convert Mathematica expressions back, `Mathematica.getexpr` should be implemented.

```julia
using Mathematica
import Mathematica
import Mathematica: getexpr

Mathematica.headstype["Less"] = Less
getexpr(::Type{Less}, e) = Less(getexpr(e.args[1]), getexpr(e.args[2]))
```
This should work so long as either `math` is on the path (normally true on linux). `Mathematica.jl` will also look for `math.exe` on Windows, which should work for Mathematica versions 8 or 9 installed in default locations. If it doesn't work for you, open an issue (in particular I don't know how this will behave on Macs).

## Current Limitations / Planned Features
* Error handling: Error checking is currently reasonable, but the only way to reset the current link once an error is encountered is to restart Julia.
* Passing native arrays and matrices is not currently supported.
* MRefs: see the MVars section of [clj-mma](https://github.com/one-more-minute/clj-mma?source=c#mathematica-vars)
* Connect to a running session and injecting callbacks to Julia functions would be really cool, but would probably require a C extension for Mathematica.
And then the custom type should work seamlessly with `weval` function.

## Current Limitations

- `Mathematica.jl` currently cannot handle internal errors. If an error happens, the only way to recover is to restart the Julia session.

## Planned Features

- [ ] Conversion between `Expr` and `WExpr`
Binary file added img/display.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
68 changes: 10 additions & 58 deletions src/Mathematica.jl
Original file line number Diff line number Diff line change
@@ -1,63 +1,15 @@
module Mathematica

using MathLink
# This package is based on MathLink
import MathLink
import MathLink: WExpr, WSymbol
import IterTools: takewhile

const exprs = quote
export weval, @importsymbol

# Typed Mathematica functions to import, in alphabetical order.
D::Expr
Integrate::Expr
Prime(Integer)::Int
RandomReal(Number)::Float64
RandomReal(Number, Integer)::Vector{Float64}
ToString::UTF8String
include("types.jl")
include("operators.jl")
include("show.jl")
include("eval.jl")

end

const macros = quote

# Macros to import, in alphabetical order.
Integrate::Expr
D::Expr

end

# -----------
# Import Code
# -----------

getsym(expr) = typeof(expr) == Symbol ? expr : getsym(expr.args[1])
macrosym(s) = Symbol(string("@", s))

# Functions

for expr in exprs.args
if typeof(expr) == Symbol || (typeof(expr) == Expr && expr.head != :line)
@eval @mmimport $(expr)
eval(Expr(:export, getsym(expr)))
end
end

for expr in macros.args
if typeof(expr) == Symbol || (typeof(expr) == Expr && expr.head != :line)
@eval @mmacro $(expr)
eval(Expr(:export, macrosym(getsym(expr))))
end
end

for name in @math Names("System`*")
f = Symbol(name)
mf = macrosym(name)

if !isdefined(f)
@eval @mmimport $f
eval(Expr(:export, f))
end

if !isdefined(mf)
@eval @mmacro $f
eval(Expr(:export, mf))
end
end

end
end # module
30 changes: 30 additions & 0 deletions src/eval.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
weval(e)

Evaluate a Wolfram expression in Mathematica.
"""
weval(e) = MathLink.weval(buildexpr(e)) |> getexpr

"""
@importsymbol Get, N, Plot

Import Mathematica symbols as normal Julia functions.

Note: This will eagerly evaluate the function.
Some expressions involving evaluation control (like `Hold`) may not work as expected.
"""
macro importsymbol(arg)
if arg isa Symbol
list = [arg]
elseif arg.head == :tuple
list = arg.args
else
error("Not recgonized arg")
end
decls = []
for sym in list
e = :($(esc(sym))(args...) = weval($(Expr(:quote, sym))(args...)))
push!(decls, e)
end
Expr(:block, decls...)
end
40 changes: 40 additions & 0 deletions src/operators.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
(s::Symbol)(args...) = WExpr(buildexpr(s), buildexpr.(args))

function buildwith(head, a, b)
if isa(a, WExpr) && a.head == WSymbol(head)
WExpr(WSymbol(head), (a.args..., buildexpr(b)))
else
WExpr(WSymbol(head), buildexpr.((a, b)))
end
end

const WTypes = Union{WExpr, WSymbol, MathLink.WInteger, MathLink.WReal, Symbol}

#
# Arithematic operators
#

Base.:+(a::Union{WTypes, T}, b::Union{WTypes, T}) where T <: Number =
buildwith("Plus", a, b)

Base.:-(a::WTypes) =
WSymbol("Times")(-1, buildexpr(a))

Base.:-(a::Union{WTypes, T}, b::Union{WTypes, T}) where T <: Number =
a + (-b)

Base.:*(a::Union{WTypes, T}, b::Union{WTypes, T}) where T <: Number =
buildwith("Times", a, b)

Base.:/(a::Union{WTypes, T}, b::Union{WTypes, T}) where T <: Number =
a * WSymbol("Power")(buildexpr(b), -1)

Base.:^(a::Union{WTypes, T}, b::Union{WTypes, T}) where T <: Number =
WSymbol("Power")(buildexpr(a), buildexpr(b))

#
# Functions
#

Base.:|>(x, f::Symbol) = buildexpr(f)(buildexpr(x))
Base.inv(x::WTypes) = WSymbol("Inverse")(buildexpr(x))
11 changes: 11 additions & 0 deletions src/show.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
function Base.show(io::IO, mime::Union{MIME"image/svg+xml", MIME"text/latex"}, x::WExpr)
if mime == MIME"text/latex"() && x.head != WSymbol("Graphics")
type = "TeXFragment"
elseif mime == MIME"image/svg+xml"() && x.head == WSymbol("Graphics")
type = "SVG"
else
throw(MethodError(Base.show, (io, mime, x)))
end
out = weval(:ExportString(:HoldForm(x), type))
write(io, out)
end
Loading