authors: Christian Weilbach
Raster is a typed multiple dispatch system for Clojure — think Julia-style polymorphic arithmetic with JVM bytecode compilation. You write math with deftm, and Raster compiles it to code that runs at Java speed while keeping full REPL interactivity.
(ns raster.getting-started
(:require [raster.core :refer [deftm ftm defvalue]]
[raster.numeric :as n]
[raster.arrays :refer [aget aset alength aclone]]
[scicloj.kindly.v4.kind :as kind]))deftm and ftmdeftm is Raster's replacement for defn in numerical code. It looks like a normal function with type annotations:
(deftm add-scaled [x :- Double, y :- Double, alpha :- Double] :- Double
(+ x (* alpha y)))#'raster.getting-started/add-scaled
(add-scaled 1.0 2.0 3.0) ;; => 7.07.0
The :- Double annotations tell Raster the parameter and return types. Behind the scenes, deftm does much more than defn:
compile-aotNone of this happens with plain defn — which boxes all arguments through Object and is invisible to the Raster compiler.
deftm supports Julia-style multiple dispatch. Define the same function name with different type signatures, and Raster picks the most specific match at runtime:
(deftm dot-product
[a :- (Array double), b :- (Array double)] :- Double
(let [n (alength a)]
(loop [i (int 0), acc 0.0]
(if (>= i n)
acc
(recur (unchecked-inc-int i)
(+ acc (* (aget a i) (aget b i))))))))#'raster.getting-started/dot-product
(dot-product (double-array [1 2 3]) (double-array [4 5 6])) ;; => 32.032.0
You can add a Float32 version later — the dispatch system routes calls to the right implementation automatically:
(deftm dot-product
[a :- (Array float), b :- (Array float)] :- Float
(let [n (alength a)]
(loop [i (int 0), acc (float 0)]
(if (>= i n)
acc
(recur (unchecked-inc-int i)
(+ acc (* (aget a i) (aget b i))))))))#'raster.getting-started/dot-product
(dot-product (float-array [1 2 3]) (float-array [4 5 6])) ;; => 32.0 (float)32.0
ftmftm is to deftm what fn is to defn — an anonymous typed function. Use it for inline lambdas passed as arguments:
(let [scale (ftm [x :- Double, factor :- Double] :- Double
(* x factor))]
(scale 3.14 2.0)) ;; => 6.286.28
ftm generates the typed interface (zero-boxing calls), but does not register a dispatch table entry, store a walked body, or queue for lazy JIT. This is intentional — anonymous functions don't need compiler integration since they're not referenced by name.
Rule of thumb: If it has a name, use deftm. If it's a throwaway lambda passed to a solver or combinator, use ftm. Never write (def x (ftm ...)) — that gives you the worst of both worlds (named but invisible to the compiler).
defvaluedefvalue creates immutable value types with typed fields — similar to Java records but with Valhalla value semantics (when running on a Valhalla JDK):
(defvalue Point2D [x :- Double, y :- Double])#'raster.getting-started/aset-point2-d!
(let [p (->Point2D 3.0 4.0)]
{:x (.x p)
:y (.y p)
:dist (n/sqrt (+ (* (.x p) (.x p)) (* (.y p) (.y p))))}){:x 3.0, :y 4.0, :dist 5.0}
raster.numericraster.numeric provides +, -, *, /, sqrt, pow, etc. that dispatch on argument types. They work on doubles, floats, complex numbers, symbolic expressions, dual numbers (for AD), and any custom type you register:
(n/+ 1.0 2.0) ;; Double + Double → 3.03.0
(n/sqrt 2.0) ;; → 1.41421356237309511.4142135623730951
(n/pow 2.0 10.0) ;; → 1024.01024.0
These operators are what deftm bodies use for arithmetic. The walker resolves them to the optimal concrete implementation at compile time.
raster.arrays provides typed aget, aset, alength, aclone that work with any array type (double[], float[], long[], etc.) and compile to the correct JVM array instructions:
(let [a (double-array [10 20 30 40 50])]
{:length (alength a)
:third (aget a 2)
:sum (loop [i (int 0), s 0.0]
(if (>= i (alength a)) s
(recur (unchecked-inc-int i) (+ s (aget a i)))))}){:length 5, :third 30.0, :sum 150.0}
When you call a deftm function, three things can happen:
First call: The lazy JIT compiles the function body to optimized JVM bytecode (branch folding, int-counted loops, typed field access). This is a one-time cost, then subsequent calls run at Java speed.
Subsequent calls: The compiled bytecode runs directly. C2 (the HotSpot JIT) can further optimize and inline.
compile-aot: For production hot paths, you can AOT-compile an entire call chain into one method. This inlines all deftm calls, eliminates dispatch overhead, and typically matches hand-written Java.
The key insight: the same code works at all three levels. You develop interactively at the REPL, and the compiler handles the rest.