FieldML cheatsheet
FieldML is a small, statically typed expression language with tools for making geometry. This cheatsheet lists its syntax and the types of the names in its prelude. For a guided introduction, try FieldML Examples.
Types are written the way they read: -> is right associative, so Float -> Float -> Vec2 is a function of two arguments. A type in argument position keeps its parentheses, as in ('a -> 'b) -> ['a] -> ['b].
Shape of a box
Top-level let bindings stack up, and the last expression is the box's result.
let width = 40.0
let height = 25.0
{ area = width * height, square = width == height }
Syntax
| Example | What it means |
|---|---|
let x = 10 |
Binding. Gives a value a name for the expressions that follow. This binds x to the integer 10. |
let add a b = a + b |
Function. Defines a curried function: add 5 is itself a function, waiting for its second argument. |
let x = 6 in x * 7 |
Local binding. Limits the name to the expression after in. This expression evaluates to 42. |
fun x -> x + 1 |
Anonymous function. Produces a function without naming it. Pass it to a helper such as map to transform each element. |
add 20 22 |
Application. Calls a function with space-separated arguments; this returns 42 using add above. Parenthesize an argument that is itself an expression. |
10 |> add 5 |
Pipe. Passes the left value as the final argument of the call on the right. This is add 5 10, which returns 15. |
if x > 0 then "up" else "down" |
Conditional. Chooses one branch and returns its value. Both branches must have the same type; here the result is a String. |
match v with | Some x -> x | None -> 0 |
Match. Tests the shape of a value and binds its contents. With the Option type below, Some 7 returns 7 and None returns 0. |
type Option a = None | Some a |
Data type. Declares two constructors: None carries no payload, while Some carries a value of type a. Declare the type before using its constructors. |
(1, "a", true) |
Tuple. Groups a fixed number of values, whose types may differ. Use a tuple pattern to extract its parts. |
[1, 2, 3] |
List. Holds any number of elements of one type. Use map, filter and fold to process them. |
{ x = 1.0, y = 2.0 } |
Record. Groups values under field names. Bind this to point and read its coordinates with point.x and point.y. |
() |
Unit. The single value of type Unit; use it when a call has no meaningful value to supply. It is distinct from JVM null (hostNull). |
point.x |
Field access. Reads a named field from a record or a supported host value, such as a vector coordinate. |
// line(* nested block *) |
Comments. // continues to the end of the line. A block comment can span lines and contain other block comments. |
Operators
Listed loosest to tightest. Everything binds looser than function application, so print x * 10 is (print x) * 10.
| Operators | Meaning | Notes |
|---|---|---|
|| |
boolean or | Skips the right side when the left side is true. |
&& |
boolean and | Skips the right side when the left side is false. |
== != < <= > >= |
comparison | Compares values and returns a Boolean. Equality supports Int, Float, Bool and String. |
:: |
prepend to a list | Adds one element at the front. It associates right, so 1 :: 2 :: [] is [1, 2]. |
+ - ++ |
add, subtract, join strings | + and - also add or subtract vectors component by component. Use ++ to join strings. |
* / |
multiply, divide | Multiplies or divides numbers; a vector can also be scaled by a Float. |
- (prefix) |
negate | Reverses the sign of a number or each vector component. Parenthesize a negative function argument. |
|> |
pipe the left value into the right function | 10 |> add 5 is add 5 10 |
Conventions
- Single-subject helpers take the subject last, so they read at the end of a pipe:
7 |> mod 4is 3,3.0 |> pow 2.0is 9,[1] |> append [2]is[1, 2]. IntandFloatnever mix silently.10 + 1.0is a type error; usetoFloat.- Only
let f x = ...is recursive. A plainlet x = ...refers to an outerx, so shadowing keeps working. - Recursion is not tail-call optimised. Long loops should use
range/tabulatewithmap/fold. - Prefix
-binds looser than application, sof -1.0is a subtraction. Writef (-1.0).
Numbers
Angles are in radians. abs, sign, min, max and clamp keep whatever numeric type they are given; the trigonometric, exponential and rounding functions use Float. Integer division and modulo, conversions and Boolean negation have their own types below.
| Name and type | What it does |
|---|---|
piFloat |
Half a turn in radians, approximately 3.14159. |
tauFloat |
One full turn in radians, equal to 2.0 * pi. |
sinFloat -> Float |
Sine of an angle in radians. sin 0.0 is 0.0. |
cosFloat -> Float |
Cosine of an angle in radians. cos 0.0 is 1.0. |
tanFloat -> Float |
Tangent of an angle in radians. Values grow large near odd multiples of pi / 2.0. |
asinFloat -> Float |
Inverse sine, returning an angle in radians. The real-valued input range is −1 to 1. |
acosFloat -> Float |
Inverse cosine, returning an angle in radians. The real-valued input range is −1 to 1. |
atanFloat -> Float |
Inverse tangent, returning an angle in radians. |
atan2Float -> Float -> Float |
atan2 y x gives the angle of the vector (x, y), retaining the quadrant. |
sqrtFloat -> Float |
Square root. sqrt 9.0 is 3.0; a negative input produces NaN. |
expFloat -> Float |
Raises the natural base e to the supplied power. exp 0.0 is 1.0. |
logFloat -> Float |
Natural logarithm. log 1.0 is 0.0. |
floorFloat -> Float |
Rounds down to an integer-valued Float. floor (-1.2) is -2.0. |
ceilFloat -> Float |
Rounds up to an integer-valued Float. ceil 1.2 is 2.0. |
roundFloat -> Float |
Rounds to the nearest integer-valued Float; ties round toward positive infinity. |
powFloat -> Float -> Float |
pow exponent value raises the value to the exponent. 3.0 |> pow 2.0 is 9.0. |
fmodFloat -> Float -> Float |
fmod divisor value gives the floating-point remainder using modulo semantics. The divisor comes first for piping. |
lerpFloat -> Float -> Float -> Float |
lerp from to amount interpolates between two numbers. Amounts outside 0–1 extrapolate. |
abs'a -> 'a |
Absolute value, preserving Int or Float. abs (-3) is 3. |
sign'a -> 'a |
Returns −1, 0 or 1 in the input numeric type, according to its sign. |
min'a -> 'a -> 'a |
Returns the smaller of two numbers of the same type. |
max'a -> 'a -> 'a |
Returns the larger of two numbers of the same type. |
clamp'a -> 'a -> 'a -> 'a |
clamp low high value bounds a number to an interval. 5.0 |> clamp 0.0 1.0 is 1.0. |
modInt -> Int -> Int |
mod divisor value gives integer modulo. 7 |> mod 4 is 3. |
idivInt -> Int -> Int |
idiv divisor value performs integer division, truncating toward zero. 7 |> idiv 2 is 3. |
toFloatInt -> Float |
Converts an integer to a floating-point number. Use before combining it with Float values. |
toIntFloat -> Int |
Converts a floating-point number to an integer, truncating toward zero. |
notBool -> Bool |
Reverses a Boolean. not true is false. |
lerp from to amount and clamp low high value both end with the subject, so 0.5 |> lerp 0.0 10.0 is 5 and 5.0 |> clamp 0.0 1.0 is 1.
Lists
range start end stops before end; linspace start end count includes both ends. Together with tabulate they are how FieldML iterates, since there is no loop statement.
| Name and type | What it does |
|---|---|
rangeInt -> Int -> [Int] |
range start end produces consecutive integers, excluding end. range 0 3 is [0, 1, 2]. |
linspaceFloat -> Float -> Int -> [Float] |
linspace start end count produces evenly spaced values including both endpoints. A count of 1 returns only start. |
tabulate(Int -> 'a) -> Int -> ['a] |
tabulate f count calls f on each index from 0 up to, but excluding, count. |
map('a -> 'b) -> ['a] -> ['b] |
Applies a function to each element, preserving list order. [1, 2] |> map (fun x -> x + 1) is [2, 3]. |
mapIndexed(Int -> 'a -> 'b) -> ['a] -> ['b] |
Calls the function with the zero-based index followed by the element. Use it when a transformation depends on position. |
filter('a -> Bool) -> ['a] -> ['a] |
Keeps only elements for which the predicate returns true, preserving their order. |
fold('a -> 'b -> 'a) -> 'a -> ['b] -> 'a |
fold step initial list accumulates left to right. Each step receives the accumulator, then the element. |
length['a] -> Int |
Counts the elements. length [] is 0. |
isEmpty['a] -> Bool |
Returns true if the list has no elements. Check this before reading its head or tail. |
head['a] -> 'a |
Returns the first element. An empty list is a runtime error. |
tail['a] -> ['a] |
Returns every element except the first. An empty list is a runtime error. |
nthInt -> ['a] -> 'a |
nth index list reads a zero-based position. A negative or out-of-range index is a runtime error. |
takeInt -> ['a] -> ['a] |
Keeps at most the first count elements. A non-positive count returns an empty list. |
dropInt -> ['a] -> ['a] |
Removes the first count elements. A non-positive count leaves the list unchanged. |
reverse['a] -> ['a] |
Returns the elements in reverse order. |
append['a] -> ['a] -> ['a] |
append suffix list places the suffix after the subject. [1] |> append [2] is [1, 2]. |
concat[['a]] -> ['a] |
Flattens one level of nested lists. concat [[1], [2, 3]] is [1, 2, 3]. |
zip['a] -> ['b] -> [('b, 'a)] |
Pairs the subject list with the supplied list, stopping at the shorter length. The piped element comes first in each pair. |
sortBy('a -> Float) -> ['a] -> ['a] |
Sorts ascending by a floating-point key computed for each element. |
sum[Float] -> Float |
Adds a list of Float values. The sum of an empty list is 0.0. |
head, tail and nth fail at run time on a list that is too short. zip pairs the piped list first, so [1, 2] |> zip ["a", "b"] is [(1, "a"), (2, "b")].
range 0 6
|> filter (fun i -> mod 2 i == 0)
|> map (fun i -> toFloat i / 5.0)
Vectors
Vec2, Vec3 and Vec4 add and subtract component by component, multiply either component-wise or by a Float, divide by a Float, and read out through .x, .y, .z and .w. In dot, vlength, distance, normalize and mix the variable 'a stands for any one of the three widths; both sides have to agree, and mixing widths is a type error.
| Name and type | What it does |
|---|---|
vec2'a -> 'b -> Vec2 |
Constructs a two-component vector from x and y. Each component accepts an Int or Float. |
vec3'a -> 'b -> 'c -> Vec3 |
Constructs a three-component vector from x, y and z. Each component accepts an Int or Float. |
vec4'a -> 'b -> 'c -> 'd -> Vec4 |
Constructs a four-component vector from x, y, z and w; also used for RGBA colours. |
dot'a -> 'a -> Float |
Returns the scalar dot product of two vectors of the same width. |
vlength'a -> Float |
Returns the Euclidean length of a vector. vlength (vec2 3 4) is 5.0. |
distance'a -> 'a -> Float |
Returns the Euclidean distance between two vectors of the same width. |
normalize'a -> 'a |
Returns a unit-length vector in the same direction. A zero vector is left unchanged. |
mix'a -> 'a -> Float -> 'a |
mix a b amount interpolates component by component. Both vectors must have the same width; the amount is not clamped. |
crossVec3 -> Vec3 -> Vec3 |
Returns the oriented cross product of two three-component vectors. Argument order matters. |
polarFloat -> Float -> Vec2 |
polar angle radius converts polar coordinates to a Vec2. The angle is in radians. |
The arguments of vec2, vec3 and vec4 are numeric rather than strictly Float, so vec2 0 1 and vec2 0.0 1.0 are both fine. polar angle radius is where most generated geometry starts.
let spoke i count =
polar (tau * toFloat i / toFloat count) 50.0
range 0 4 |> map (fun i -> spoke i 4) |> length
Lines
Path and style helpers all take the FLine last and return it, so they chain in a pipe. Coordinates are numeric, and moveTo3/lineTo3 are the three-dimensional forms.
| Name and type | What it does |
|---|---|
flineFLine |
Produces a fresh empty path each time it is evaluated. Use it at the start of a path-building pipe. |
moveTo'a -> 'b -> FLine -> FLine |
moveTo x y line starts a new contour at a two-dimensional point, without drawing a segment. |
lineTo'a -> 'b -> FLine -> FLine |
lineTo x y line adds a straight segment to a two-dimensional point. |
moveTo3'a -> 'b -> 'c -> FLine -> FLine |
moveTo3 x y z line starts a new contour at a three-dimensional point. |
lineTo3'a -> 'b -> 'c -> FLine -> FLine |
lineTo3 x y z line adds a straight segment to a three-dimensional point. |
moveToVVec2 -> FLine -> FLine |
Starts a new contour at a Vec2 point. Useful when geometry is already represented as vectors. |
lineToVVec2 -> FLine -> FLine |
Adds a straight segment to a Vec2 point. Use it in a fold over a list of points. |
cubicTo'a -> 'b -> 'c -> 'd -> 'e -> 'f -> FLine -> FLine |
Takes two control points (x0, y0) and (x1, y1), then the endpoint (x, y), then the path. |
closeFLine -> FLine |
Closes the current contour back to its start and returns the path. |
thickness'a -> FLine -> FLine |
Sets the path’s stroke thickness and returns the same path. |
colorVec4 -> FLine -> FLine |
Sets the path’s RGBA colour from a Vec4. The fourth component is opacity. |
filledBool -> FLine -> FLine |
Enables or disables filling the path’s interior. |
strokedBool -> FLine -> FLine |
Enables or disables drawing the path’s outline. |
pointedBool -> FLine -> FLine |
Enables or disables drawing the path’s points. |
pointSizeFloat -> FLine -> FLine |
Sets the size of rendered points. Enable them with pointed true. |
Folding a list of points into fline is the idiom for generated geometry: the accumulator is the line, and each step adds one instruction.
linspace 0.0 tau 24
|> map (fun a -> polar a 30.0)
|> fold (fun line point -> line |> lineToV point) fline
|> close
|> stroked true
Output and animation
print, println, display, draw and the tap family all return what they were given, so they sit in the middle of a pipe without breaking it.
| Name and type | What it does |
|---|---|
print'a -> 'a |
Prints the value without adding a newline and returns it unchanged. |
println'a -> 'a |
Prints the value followed by a newline and returns it unchanged. |
displayString -> 'a -> 'a |
display channel value sends the value to a named output area, then returns it unchanged. |
show'a -> String |
Formats a value as a string without sending it to an output area. |
tap('a -> 'b) -> 'a -> 'a |
tap effect value calls the effect and returns the original value, discarding the effect’s result. |
drawString -> FLine -> FLine |
draw channel line displays a path in a named output area and returns the path. |
drawSizedString -> Float -> Float -> FLine -> FLine |
drawSized channel width height line displays a path with an explicit output size. |
animate(Float -> 'a) -> Host |
Returns an animation that Field drives after the box is launched. The function receives elapsed seconds each frame. |
animateForFloat -> (Float -> 'a) -> Host |
animateFor seconds f runs the same animation model with a finite duration. |
UnitUnit |
The named unit value, equivalent to (). It is not JVM null. |
animate returns a value Field drives once per frame, passing the seconds since the box was launched, until the box is stopped. The function runs for its effects, so draw inside it; animateFor is the same with a stopping time.
JVM interop
Use hostTap when a Kotlin function is an effect in the middle of a pipe: it looks the function up in Field's source index, fills in defaults, and puts the piped value in the data argument. hostCall covers everything else. See The JVM bridge for the mechanism and FieldML Examples for worked versions.
| Name and type | What it does |
|---|---|
thisBoxBox |
The Field box evaluating this program. Use it as the target of host member calls or reads. |
hostTapString -> 'a -> 'b -> 'b |
hostTap name arguments value finds a Kotlin helper through Field’s source index, applies the effect and returns the piped value. |
hostInputHost |
A placeholder for the piped input inside an argument structure used by hostStaticTap. |
hostNullHost |
JVM null, for a nullable host argument. Use this instead of Unit when a method requires an absent value. |
hostClassString -> Host |
Resolves a fully qualified JVM class name for construction or static calls. |
hostNew'a -> Host -> Host |
hostNew arguments class calls a constructor. Tuples and lists spread positionally; records supply named arguments and allow omitted defaults. |
hostNew0Host -> Host |
Calls the class’s zero-argument constructor. |
hostCallString -> 'a -> 'b -> Host |
hostCall name arguments target invokes an instance method. Use a record for Kotlin named arguments and defaults. |
hostCall0String -> 'a -> Host |
hostCall0 name target invokes a method with no arguments. |
hostCall1String -> 'a -> 'b -> Host |
hostCall1 name argument target passes exactly one argument. A list or record remains one value rather than being spread. |
hostLaunchString -> 'a -> 'b -> (Host -> 'c) -> Host |
hostLaunch name arguments target then calls a Kotlin suspend method and passes its result to then when it returns. It returns the call's Job at once. |
hostAwaitString -> 'a -> 'b -> Host |
hostAwait name arguments target calls a Kotlin suspend method and waits for it, inside a body Kotlin runs as a suspend function (frames, eachFrame, loop). hostAwait "frame" () f waits one frame. |
hostCall2String -> 'a -> 'b -> 'c -> Host |
hostCall2 name a b target invokes a method with exactly two arguments. |
hostStaticCallString -> 'a -> Host -> Host |
hostStaticCall name arguments class invokes a static JVM method. Argument spreading follows hostCall. |
hostExtHost -> String -> 'a -> 'b -> Host |
hostExt class name arguments receiver invokes a Kotlin extension compiled into that class. The piped receiver is separate from the written arguments. |
hostStaticTapString -> 'a -> Host -> 'b -> 'b |
Calls a static method, substituting the piped value for hostInput in its arguments. Returns the piped value rather than the method result. |
hostGetString -> 'a -> Host |
hostGet name target reads a host property or field. Reflection can preserve a more precise type than the static Host signature. |
hostSetString -> 'a -> 'b -> Host |
hostSet name value target writes a host property or field and returns the target object. |
hostAsString -> 'a -> Host |
hostAs className value asserts a specific JVM class after type erasure. The cast is checked at runtime. |
hostAsInt'a -> Int |
Converts a compatible host value to a FieldML integer after a reflective call. |
hostAsFloat'a -> Float |
Converts a compatible host value to a FieldML floating-point number. |
hostAsBool'a -> Bool |
Converts a compatible host value to a FieldML Boolean. |
hostAsString'a -> String |
Converts the supplied value to a FieldML string. |
hostAsList'a -> [Host] |
Converts a host collection or array into a FieldML list so it can be mapped or folded. |
hostCall name arguments target takes as many arguments as the method does: a tuple, or a list written out in place, is spread; a record is spread by name, and every Kotlin parameter you leave out of it gets its declared default; anything else is a single argument. hostNew is the same for constructors. hostCall0, hostCall1 and hostCall2 fix the argument count instead; hostCall1 never spreads, so use it to pass one list or one record as a single argument. A FieldML function passed where the JVM wants a functional interface (a Kotlin lambda parameter, Runnable, Comparator, or a suspend one such as eachFrame's) is converted on the way in.
The table gives the general types. In an actual call the type checker looks the member up, so thisBox |> hostGet "width" is an Int, a misspelled member name is a type error rather than a crash, and a result whose class is known keeps that class as its type. You get Host only when the lookup cannot tell: a generic erased to Object, a nullable Kotlin return, an array. Host unifies with every type; the hostAs… functions turn it into a specific one.
Source macros
Field's box-code macros (moduleHere, prop, println and the rest) rewrite Kotlin source before it is compiled. FieldML is not compiled as Kotlin, so it reaches them through two functions. hostMacroExpand runs a macro's expansion against this box, which is where a macro compiles its shaders, seeds its properties and installs its actions, and returns the rewritten source. hostMacro does the same and then calls the function the rewritten source would have called. See The JVM bridge for which macros this reaches and which it cannot.
The prop functions are built into FieldML instead. In Kotlin, prop<T>(name) declares a named property that boxes below inherit, and FieldML reads and writes that same property. A Kotlin box's speed and a FieldML box's prop "speed" are one property.
| Name and type | What it does |
|---|---|
hostMacroExpandString -> 'a -> {call: String, arguments: String, here: String, preamble: String, declarations: String, start: String, end: String} |
Runs a box-code macro’s expansion using source-text arguments. Returns the rewritten call and its supporting source without calling it. |
hostMacroString -> 'a -> 'b -> Host |
Expands a macro, then calls its rewritten target with the supplied runtime values. Expansion arguments are source text; call arguments are values. |
propString -> Host |
Reads a named box property, searching this box and its ancestors. A missing property is an error. |
propOrString -> 'a -> Host |
propOr name fallback reads a property through the same inheritance chain and uses the fallback if it is absent. |
setPropString -> 'a -> 'a |
setProp name value writes locally and returns the value. This shadows an ancestor’s property without changing it. |
propUpString -> Host |
Reads an inherited slot; if absent, runs an upstream Kotlin or FieldML supplier before reading again. |
propDownString -> Host |
Reads an inherited slot and records the reader for a later reactive write. |
propUpDownString -> Host |
Combines the pull on a missing read with a recorded downstream reader. |
setPropUpString -> 'a -> 'a |
Writes a local slot that an upstream pull can find in this FieldML box. |
setPropDownString -> 'a -> 'a |
Writes locally; a changed value reruns boxes that read the old value. |
setPropUpDownString -> 'a -> 'a |
Combines an upstream supplier with a changed-write cascade. |
rootPropString -> Host |
Reads a named property from the document root. |
setRootPropString -> 'a -> 'a |
Writes a named property on the document root and returns the value, making it available through inheritance. |
onceString -> ('a -> 'b) -> Host |
once name f computes and stores a value in this box’s own slot on first use. Later reads reuse that stored value; a JVM-null result is not cached. |
A macro's arguments are source text, one string per piece: hostMacroExpand "prop" ("<Double>", "(speed)") stands for the Kotlin prop<Double>(speed). hostMacro takes the same text and then the values to call with: hostMacro "println" "(x)" (7, "hello") expands println, then calls the function it expands to with line 7 and one value.
Reading a prop that no box up the graph has set is an error, usually because the box that sets it has not run yet. propOr takes a fallback instead. Writing with setProp is always local: it shadows an ancestor's value for this box and everything below it rather than changing theirs. setRootProp puts the slot on the document root, so every box shares the one value. once computes on the first read and stores the result, so the second read does not recompute.
For reactive properties, the read and write forms share the same slot as prop and setProp.
Write setPropUp "speed" 3 in a supplier box, then propUp "speed" in a child: a missing
read runs the supplier first. Use the Down forms when changed writes should rerun boxes that
previously read the slot. A writer that needs both directions uses setPropUpDown.