millionchart
فارسی

Functions, types and enums

Write your own functions with fn, return values early or several at once, add methods to a type, and know how state works inside a function.

A function packs a calculation under a name. Write it with fn, put it above the code that uses it, and call it like a built-in.

A function#

A function with a block returns its last expression, or the value of a return. A function written on one line uses =.

Cuprel
indicator "Two functions"
    edition = 1

fn range_percent(length: int): float
    let top = tech.highest(high, length)
    let bottom = tech.lowest(low, length)
    return (top - bottom) / bottom * 100
fn middle(a: float, b: float): float = (a + b) / 2
plot(range_percent(20), name = "20-bar range, %")
plot(middle(high, low), name = "Middle of the bar")

The types after the names are optional documentation for the reader. In edition 1 the engine does not check them: (length: int): float tells a reader that the function takes a whole number and gives a number, but a text passed in is not refused.

A parameter can have a default, and an argument can be passed by name:

Cuprel
indicator "Defaults and names"
    edition = 1

fn scaled(x: float, factor: float = 2.0): float = x * factor
plot(scaled(close), name = "Doubled")
plot(scaled(close, factor = 0.5), name = "Halved")

Leaving early#

return ends the function at once, wherever it is. A function can use it as a guard:

Cuprel
indicator "A guard"
    edition = 1

fn safe_ratio(a: float, b: float): float
    if b == 0
        return na
    return a / b
plot(safe_ratio(close - open, high - low), name = "Body as a share of the range")
Cuprel
indicator "Absolute value"
    edition = 1

fn absolute(x: float): float
    return if x >= 0 then x else -x
plot(absolute(close - open), name = "Body size")

A function cannot call itself#

A function may call other functions, but not itself, and not through a chain of calls that comes back to it. The checker refuses it with E3310 and names the chain. The reason is that the depth of such a chain would then be decided by the machine that runs the script, so the same script could work on one machine and stop on another. Use a loop, or keep the running value in a state (see State inside a function, below).

Cuprel · error E3310
indicator "A function that calls itself"
    edition = 1

fn total(n: int): int = if n < 1 then 0 else n + total(n - 1)
plot(total(5), name = "Sum")

Corrected:

Cuprel
indicator "A loop instead"
    edition = 1

fn total(n: int): int
    mut sum = 0
    for i in 1 to n
        sum += i
    return sum
plot(total(5), name = "Sum")

Functions that call one another without coming back to the first are fine. The engine also counts how many of your functions are in progress at once, one inside another, and stops a bar that goes past 64 with E-LIMIT-DEPTH; see Limits and budgets.

Several values at once#

A function can return a tuple, and the caller takes it apart. The number of names must match the number of values.

Cuprel
indicator "Channel"
    edition = 1
    display = price

fn channel(length: int): (float, float, float)
    let middle = tech.sma(close, length)
    let spread = tech.stdev(close, length) * 2
    return (middle + spread, middle, middle - spread)
let (upper, middle, lower) = channel(20)
plot(upper, name = "Upper", color = color.red)
plot(middle, name = "Middle", color = color.gray)
plot(lower, name = "Lower", color = color.green)
Cuprel · error E3143
indicator "Too many names"
    edition = 1

fn pair(): (float, float) = (1.0, 2.0)
let (a, b, c) = pair()
plot(a)

Use _ for a value you do not need: let (upper, _, lower) = channel(20). It also works as a loop variable: for _ in 0 to 4.

State inside a function#

A function that uses tech.* or a state keeps a separate memory for every place it is called from. Two calls are two memories:

Cuprel
indicator "Two calls, two memories"
    edition = 1

fn smoothed(x: float): float = tech.ema(x, 5)
plot(smoothed(close), name = "Smoothed close")
plot(smoothed(open), name = "Smoothed open")

The two plots do not interfere: each call remembers its own series. A function may not change a mut or state declared outside it (E3104); give it the value as an argument and take the new value as its result. Variables shows the error.

Types with methods#

A type has fields. A function written as fn TypeName.method(self, ...) is a method of that type, and is called with a dot.

Cuprel
indicator "A type with a method"
    edition = 1

type Level
    price: float
    width: float = 1.0
fn Level.top(self): float = self.price + self.width
fn Level.bottom(self): float = self.price - self.width
let level_ = Level(price = close, width = 2.0)
plot(level_.top(), name = "Top", color = color.red)
plot(level_.bottom(), name = "Bottom", color = color.green)

You build a record by naming its fields. You can also give a built-in type a method of your own, such as fn array.size(self). Yours then wins over the built-in method of the same name, and the checker warns about it (W-METHOD-SHADOWS-BUILTIN) so you know it happened.

The same dot form calls a text function on a text: "10:30".split(":") is text.split("10:30", ":").

Enums#

An enum is a closed list of names, written one per line. A member may carry a label:

Cuprel
indicator "An enum input"
    edition = 1

enum Speed
    fast = "Fast"
    normal = "Normal"
    slow = "Slow"
input speed: Speed = Speed.normal, name = "Speed"
let length = match speed
    Speed.fast => 8
    Speed.normal => 21
    Speed.slow => 55
plot(tech.ema(close, length), name = "Average")

The settings panel shows an enum input as a dropdown with the labels. match on an enum is the way to act on it. Choices and loops covers match.

See also: Values and types, Modules and libraries, Inputs.