& this file, uniquely, gets `base` loaded as context. See src/base.janet for exports & some forward declarations & TODO: fix this so that we don't need (as many of) them fn and fn append fn apply_command fn assoc fn atan/2 fn deg/rad fn dict fn first fn floor fn get fn join fn mod fn neg? fn print! fn some? fn store! fn string fn turn/rad fn unbox fn update! & the very base: know something's type fn type { "Returns a keyword representing the type of the value passed in." (x) -> base :type (x) } & some helper type functions fn coll? { "Returns true if a value is a collection: dict, list, tuple, or set." (coll as :dict) -> true (coll as :list) -> true (coll as :tuple) -> true (coll as :set) -> true (_) -> false } fn ordered? { "Returns true if a value is an indexed collection: list or tuple." (coll as :list) -> true (coll as :tuple) -> true (coll as :string) -> true (_) -> false } fn assoc? { "Returns true if a value is an associative collection: a dict or a pkg." (d as :dict) -> true (p as :pkg) -> true (_) -> false } &&& nil: working with nothing fn nil? { "Returns true if a value is nil." (nil) -> true (_) -> false } fn some? { "Returns true if a value is not nil." (nil) -> false (_) -> true } fn some { "Takes a possibly nil value and a default value. Returns the value if it's not nil, returns the default if it's nil." (nil, default) -> default (value, _) -> value } & ...and if two things are the same fn eq? { "Returns true if all arguments have the same value." (x) -> true (x, y) -> base :eq? (x, y) (x, y, ...zs) -> if eq? (x, y) then loop (y, zs) with { (a, []) -> eq? (a, x) (a, [b, ...cs]) -> if eq? (a, x) then recur (b, cs) else false } else false } &&& true & false: boolean logic (part the first) fn bool? { "Returns true if a value is of type :boolean." (false) -> true (true) -> true (_) -> false } fn true? { "Returns true if a value is boolean `true`. Useful to distinguish between `true` and anything else." (true) -> true (_) -> false } fn false? { "Returns `true` if a value is `false`, otherwise returns `false`. Useful to distinguish between `false` and `nil`." (false) -> true (_) -> false } fn bool { "Returns false if a value is nil or false, otherwise returns true." (nil) -> false (false) -> false (_) -> true } fn not { "Returns false if a value is truthy, true if a value is falsy." (nil) -> true (false) -> true (_) -> false } fn neq? { "Returns true if none of the arguments have the same value." (x) -> false (x, y) -> not (eq? (x, y)) (x, y, ...zs) -> if eq? (x, y) then false else loop (y, zs) with { (a, []) -> neq? (a, x) (a, [b, ...cs]) -> if neq? (a, x) then recur (b, cs) else false } } & tuples: not a lot you can do with them functionally fn tuple? { "Returns true if a value is a tuple." (tuple as :tuple) -> true (_) -> false } &&& functions: getting things done fn fn? { "Returns true if an argument is a function." (f as :fn) -> true (_) -> false } & what we need for some very basic list manipulation fn rest { "Returns all but the first element of a list or tuple, as a list." ([]) -> [] (()) -> () (xs as :list) -> base :rest (xs) (xs as :tuple) -> base :rest (xs) (xs as :string) -> base :str_slice (xs, 1) } fn inc { "Increments a number." (x as :number) -> base :inc (x) } fn dec { "Decrements a number." (x as :number) -> base :dec (x) } fn count { "Returns the number of elements in a collection (including string)." (xs as :list) -> base :count (xs) (xs as :tuple) -> base :count (xs) (xs as :dict) -> base :count (xs) (xs as :string) -> base :count (xs) (xs as :set) -> base :count (xs) } fn empty? { "Returns true if something is empty. Otherwise returns false (including for things that can't logically be empty, like numbers)." ([]) -> true (#{}) -> true (s as :set) -> eq? (s, ${}) (()) -> true ("") -> true (_) -> false } fn any? { "Returns true if something is not empty, otherwise returns false (including for things that can't be logically full, like numbers)." ([...]) -> true (#{...}) -> true (s as :set) -> not (empty? (s)) ((...)) -> true (s as :string) -> not (empty? (s)) (_) -> false } fn list? { "Returns true if the value is a list." (l as :list) -> true (_) -> false } fn list { "Takes a value and returns it as a list. For values, it simply wraps them in a list. For collections, conversions are as follows. A tuple->list conversion preservers order and length. Unordered collections do not preserve order: sets and dicts don't have predictable or stable ordering in output. Dicts return lists of (key, value) tuples." (x) -> base :to_list (x) } fn fold { "Folds a list." (f as :fn, []) -> [] (f as :fn, xs as :list) -> fold (f, xs, f ()) (f as :fn, [], root) -> [] (f as :fn, xs as :list, root) -> loop (root, first (xs), rest (xs)) with { (prev, curr, []) -> f (prev, curr) (prev, curr, remaining) -> recur ( f (prev, curr) first (remaining) rest (remaining) ) } } & TODO: optimize these with base :conj! fn map { "Maps a function over a list: returns a new list with elements that are the result of applying the function to each element in the original list. E.g., `map ([1, 2, 3], inc) &=> [2, 3, 4]`." (f as :fn, xs) -> { fn mapper (prev, curr) -> append (prev, f (curr)) fold (mapper, xs, []) } (kw as :keyword, xs) -> { fn mapper (prev, curr) -> append (prev, kw (curr)) fold (mapper, xs, []) } } fn filter { "Takes a list and a predicate function, and returns a new list with only the items that produce truthy values when the function is called on them. E.g., `filter ([1, 2, 3, 4], odd?) &=> [1, 3]`." (p? as :fn, xs) -> { fn filterer (filtered, x) -> if p? (x) then append (filtered, x) else filtered fold (filterer, xs, []) } } fn keep { "Takes a list and returns a new list with any `nil` values omitted." (xs) -> filter (some?, xs) } fn append { "Adds an element to a list or set." () -> [] (xs as :list) -> xs (xs as :list, x) -> base :conj (xs, x) (xs as :set) -> xs (xs as :set, x) -> base :conj (xs, x) } fn append! { "Adds an element to a list, modifying it." () -> [] (xs as :list) -> xs (xs as :list, x) -> base :conj! (xs, x) } fn concat { "Combines two lists, strings, or sets." (x as :string, y as :string) -> base :concat (x, y) (xs as :list, ys as :list) -> base :concat (xs, ys) (xs as :set, ys as :set) -> base :concat (xs, ys) (xs, ys, ...zs) -> fold (concat, zs, concat (xs, ys)) } fn set { "Takes an ordered collection--list or tuple--and turns it into a set. Returns sets unharmed." (xs as :list) -> fold (append, xs, ${}) (xs as :tuple) -> do xs > list > set (xs as :set) -> xs } fn set? { "Returns true if a value is a set." (xs as :set) -> true (_) -> false } fn contains? { "Returns true if a set or list contains a value." (value, s as :set) -> bool (base :get (s, value)) (value, l as :list) -> contains? (value, set (list)) } fn omit { "Returns a new set with the value omitted." (value, s as :set) -> base :disj (s, value) } fn print! { "Sends a text representation of Ludus values to the console." (...args) -> { base :print! (args) :ok } } fn show { "Returns a text representation of a Ludus value as a string." (x) -> base :show (x) } fn report! { "Prints a value, then returns it." (x) -> { print! (x) x } (msg as :string, x) -> { print! (concat ("{msg} ", show (x))) x } } fn doc! { "Prints the documentation of a function to the console." (f as :fn) -> do f > base :doc > print! (_) -> :none } &&& strings: harder than they look! fn string? { "Returns true if a value is a string." (x as :string) -> true (_) -> false } fn string { "Converts a value to a string by using `show`. If it is a string, returns it unharmed. Use this to build up strings of different kinds of values." (x as :string) -> x (x) -> show (x) (x, ...xs) -> loop (x, xs) with { (out, [y]) -> concat (out, show (y)) (out, [y, ...ys]) -> recur (concat (out, show (y)), ys) } } fn join { "Takes a list of strings, and joins them into a single string, interposing an optional separator." ([]) -> "" ([str as :string]) -> str (strs as :list) -> join (strs, "") ([], separator as :string) -> "" ([str as :string], separator as :string) -> str ([str, ...strs], separator as :string) -> fold ( fn (joined, to_join) -> concat (joined, separator, to_join) strs str ) } fn split { "Takes a string, and turns it into a list of strings, breaking on the separator." (str as :string, break as :string) -> base :split (break, str) } fn trim { "Trims whitespace from a string. Takes an optional argument, `:left` or `:right`, to trim only on the left or right." (str as :string) -> base :trim (str) (str as :string, :left) -> base :triml (str) (str as :string, :right) -> base :trimr (str) } fn upcase { "Takes a string and returns it in all uppercase. Works only for ascii characters." (str as :string) -> base :upcase (str) } fn downcase { "Takes a string and returns it in all lowercase. Works only for ascii characters." (str as :string) -> base :downcase (str) } fn chars { "Takes a string and returns its characters as a list. Works only for strings with only ascii characters. Panics on any non-ascii characters." (str as :string) -> match base :chars (str) with { (:ok, chrs) -> chrs (:err, msg) -> panic! msg } } fn chars/safe { "Takes a string and returns its characters as a list, wrapped in a result tuple. Works only for strings with only ascii characters. Returns an error tuple on any non-ascii characters." (str as :string) -> base :chars (str) } fn ws? { "Tells if a string is a whitespace character." (" ") -> true ("\n") -> true ("\t") -> true (_) -> false } fn strip { "Removes punctuation from a string, removing all instances of ,.;:?!" ("{x},{y}") -> strip ("{x}{y}") ("{x}.{y}") -> strip ("{x}{y}") ("{x};{y}") -> strip ("{x}{y}") ("{x}:{y}") -> strip ("{x}{y}") ("{x}?{y}") -> strip ("{x}{y}") ("{x}!{y}") -> strip ("{x}{y}") (x) -> x } fn words { "Takes a string and returns a list of the words in the string. Strips all whitespace." (str as :string) -> { let no_punct = strip (str) let strs = split (no_punct, " ") fn worder (l, s) -> if empty? (s) then l else append (l, s) fold (worder, strs, []) } } fn sentence { "Takes a list of words and turns it into a sentence." (strs as :list) -> join (strs, " ") } fn to_number { "Takes a string that presumably contains a representation of a number, and tries to give you back the number represented. Returns a result tuple." (num as :string) -> base :to_number (num) } &&& boxes: mutable state and state changes fn box? { "Returns true if a value is a box." (b as :box) -> true (_) -> false } fn unbox { "Returns the value that is stored in a box." (b as :box) -> base :unbox (b) } fn store! { "Stores a value in a box, replacing the value that was previously there. Returns the value." (b as :box, value) -> { base :store! (b, value) value } } fn update! { "Updates a box by applying a function to its value. Returns the new value." (b as :box, f as :fn) -> { let current = unbox (b) let new = f (current) store! (b, new) } } &&& numbers, basically: arithmetic and not much else, yet & TODO: add nan?, fn number? { "Returns true if a value is a number." (x as :number) -> true (_) -> false } fn add { "Adds numbers or vectors." () -> 0 (x as :number) -> x (x as :number, y as :number) -> base :add (x, y) (x, y, ...zs) -> fold (add, zs, base :add (x, y)) & add vectors ((x1, y1), (x2, y2)) -> (add (x1, x2), add (y1, y2)) } fn sub { "Subtracts numbers or vectors." () -> 0 (x as :number) -> x (x as :number, y as :number) -> base :sub (x, y) (x, y, ...zs) -> fold (sub, zs, base :sub (x, y)) ((x1, y1), (x2, y2)) -> (base :sub (x1, x2), base :sub (y1, y2)) } fn mult { "Multiplies numbers or vectors." () -> 1 (x as :number) -> x (x as :number, y as :number) -> base :mult (x, y) (x, y, ...zs) -> fold (mult, zs, mult (x, y)) (scalar as :number, (x, y)) -> (mult (x, scalar), mult (y, scalar)) ((x, y), scalar as :number) -> mult (scalar, (x, y)) } fn div { "Divides numbers. Panics on division by zero." (x as :number) -> x (_, 0) -> panic! "Division by zero." (x as :number, y as :number) -> base :div (x, y) (x, y, ...zs) -> { let divisor = fold (mult, zs, y) div (x, divisor) } } fn div/0 { "Divides numbers. Returns 0 on division by zero." (x as :number) -> x (_, 0) -> 0 (x as :number, y as :number) -> base :div (x, y) (x, y, ...zs) -> { let divisor = fold (mult, zs, y) div/0 (x, divisor) } } fn div/safe { "Divides a number. Returns a result tuple." (x as :number) -> (:ok, x) (_, 0) -> (:err, "Division by zero") (x, y) -> (:ok, div (x, y)) (x, y, ...zs) -> { let divisor = fold (mult, zs, y) div/safe (x, divisor) } } fn inv { "Returns the inverse of a number: 1/n or `div (1, n)`. Panics on division by zero." (x as :number) -> div (1, x) } fn inv/0 { "Returns the inverse of a number: 1/n or `div/0 (1, n)`. Returns 0 on division by zero." (x as :number) -> div/0 (1, x) } fn inv/safe { "Returns the inverse of a number: 1/n or `div/safe (1, n)`. Returns a result tuple." (x as :number) -> div/safe (1, x) } fn abs { "Returns the absolute value of a number." (0) -> 0 (n as :number) -> if neg? (n) then mult (-1, n) else n } fn neg { "Multiplies a number by -1, negating it." (n as :number) -> mult (n, -1) } fn angle { "Calculates the angle between two vectors." (v1, v2) -> sub (atan/2 (v2), atan/2 (v1)) } fn zero? { "Returns true if a number is 0." (0) -> true (_) -> false } fn gt? { "Returns true if numbers are in decreasing order." (x as :number) -> true (x as :number, y as :number) -> base :gt (x, y) (x, y, ...zs) -> loop (y, zs) with { (a, [b]) -> base :gt (a, b) (a, [b, ...cs]) -> if base :gt (a, b) then recur (b, cs) else false } } fn gte? { "Returns true if numbers are in decreasing or flat order." (x as :number) -> true (x as :number, y as :number) -> base :gte (x, y) (x, y, ...zs) -> loop (y, zs) with { (a, [b]) -> base :gte (a, b) (a, [b, ...cs]) -> if base :gte (a, b) then recur (b, cs) else false } } fn lt? { "Returns true if numbers are in increasing order." (x as :number) -> true (x as :number, y as :number) -> base :lt (x, y) (x, y, ...zs) -> loop (y, zs) with { (a, [b]) -> base :lt (a, b) (a, [b, ...cs]) -> if base :lt (a, b) then recur (b, cs) else false } } fn lte? { "Returns true if numbers are in increasing or flat order." (x as :number) -> true (x as :number, y as :number) -> base :lte (x, y) (x, y, ...zs) -> loop (y, zs) with { (a, [b]) -> base :lte (a, b) (a, [b, ...cs]) -> if base :lte (a, b) then recur (b, cs) else false } } fn between? { "Returns true if a number is in the range [lower, higher): greater than or equal to the lower number, less than the higher." (lower as :number, higher as :number, x as :number) -> and ( gte? (x, lower) lt? (x, higher) ) } fn neg? { "Returns true if a value is a negative number, otherwise returns false." (x as :number) if lt? (x, 0) -> true (_) -> false } fn pos? { "Returns true if a value is a positive number, otherwise returns false." (x as :number) if gt? (x, 0) -> true (_) -> false } fn even? { "Returns true if a value is an even number, otherwise returns false." (x as :number) if eq? (0, mod (x, 2)) -> true (_) -> false } fn odd? { "Returns true if a value is an odd number, otherwise returns false." (x as :number) if eq? (1, mod (x, 2)) -> true (_) -> false } fn min { "Returns the number in its arguments that is closest to negative infinity." (x as :number) -> x (x as :number, y as :number) -> if lt? (x, y) then x else y (x, y, ...zs) -> fold (min, zs, min (x, y)) } fn max { "Returns the number in its arguments that is closest to positive infinity." (x as :number) -> x (x as :number, y as :number) -> if gt? (x, y) then x else y (x, y, ...zs) -> fold (max, zs, max (x, y)) } & additional list operations now that we have comparitors fn at { "Returns the element at index n of a list or tuple, or the byte at index n of a string. Zero-indexed: the first element is at index 0. Returns nil if nothing is found in a list or tuple; returns an empty string if nothing is found in a string." (xs as :list, n as :number) -> base :nth (n, xs) (xs as :tuple, n as :number) -> base :nth (n, xs) (str as :string, n as :number) -> { let raw = base :nth (n, str) when { nil? (raw) -> nil gte? (raw, 128) -> panic! "not an ASCII char" true -> base :str_slice (str, n, inc (n)) } } (_) -> nil } fn first { "Returns the first element of a list or tuple." (xs) if ordered? (xs) -> at (xs, 0) } fn second { "Returns the second element of a list or tuple." (xs) if ordered? (xs) -> at (xs, 1) } fn last { "Returns the last element of a list or tuple." (xs) if ordered? (xs) -> at (xs, dec (count (xs))) } fn butlast { "Returns a list, omitting the last element." (xs as :list) -> base :slice (xs, 0, dec (count (xs))) } fn slice { "Returns a slice of a list or a string, representing a sub-list or sub-string." (xs as :list, end as :number) -> slice (xs, 0, end) (xs as :list, start as :number, end as :number) -> when { gte? (start, end) -> [] gt? (end, count (xs)) -> slice (xs, start, count (xs)) neg? (start) -> slice (xs, 0, end) true -> base :slice (xs, start, end) } (str as :string, end as :number) -> base :str_slice (str, 0, end) (str as :string, start as :number, end as :number) -> base :str_slice (str, start, end) } &&& keywords: funny names fn keyword? { "Returns true if a value is a keyword, otherwise returns false." (kw as :keyword) -> true (_) -> false } & TODO: determine if Ludus should have a `keyword` function that takes a string and returns a keyword. Too many panics, it has weird memory consequences, etc. & TODO: make `and` and `or` special forms which lazily evaluate arguments fn and { "Returns true if all values passed in are truthy. Note that this does not short-circuit: all arguments are evaulated before they are passed in." () -> true (x) -> bool (x) (x, y) -> base :and (x, y) (x, y, ...zs) -> fold (and, zs, base :and (x, y)) } fn or { "Returns true if any value passed in is truthy. Note that this does not short-circuit: all arguments are evaluated before they are passed in." () -> true (x) -> bool (x) (x, y) -> base :or (x, y) (x, y, ...zs) -> fold (or, zs, base :or (x, y)) } fn assoc { "Takes a dict, key, and value, and returns a new dict with the key set to value." () -> #{} (d as :dict) -> d (d as :dict, k as :keyword, val) -> base :assoc (d, k, val) (d as :dict, (k as :keyword, val)) -> base :assoc (d, k, val) } fn dissoc { "Takes a dict and a key, and returns a new dict with the key and associated value omitted." (d as :dict) -> d (d as :dict, k as :keyword) -> base :dissoc (d, k) } fn update { "Takes a dict, key, and function, and returns a new dict with the key set to the result of applying the function to original value held at the key." (d as :dict) -> d (d as :dict, k as :keyword, updater as :fn) -> base :assoc (d, k, updater (get (k, d))) } fn keys { "Takes a dict and returns a list of keys in that dict." (d as :dict) -> do d > list > map (first, _) } fn values { "Takes a dict and returns a list of values in that dict." (d as :dict) -> do d > list > map (second, _) } fn diff { "Takes two dicts and returns a dict describing their differences. Does this shallowly, offering diffs only for keys in the original dict." (d1 as :dict, d2 as :dict) -> { let key1 = keys (d1) let key2 = keys (d2) let all = do concat (d1, d2) > set > list let diffs = loop (all, []) with { & TODO: reduce this redundancy? ([k, ...ks], diffs) -> { let v1 = get (k, d1) let v2 = get (k, d2) if eq? (v1, v2) then recur (ks, diffs) else recur (ks, append (diffs, (k, (v1, v2)))) } ([k], diffs) -> { let v1 = get (k, d1) let v2 = get (k, d2) if eq? (v1, v2) then diffs else append (diffs, (k, (v1, v2))) } } dict (diffs) } } & TODO: consider merging `get` and `at` fn get { "Takes a key, dict, and optional default value; returns the value at key. If the value is not found, returns nil or the default value." (k as :keyword) -> get (k, _) (k as :keyword, d as :dict) -> get (k, d, nil) (k as :keyword, d as :dict, default) -> base :get (k, d, default) } & TODO: add sets to this? fn has? { "Takes a key and a dict, and returns true if there is a non-`nil` value stored at the key." (k as :keyword) -> has? (k, _) (k as :keyword, d as :dict) -> do d> k > nil? } fn dict { "Takes a list or tuple of (key, value) tuples and returns it as a dict. Returns dicts unharmed." (d as :dict) -> d (l as :list) -> fold (assoc, l) (t as :tuple) -> do t > list > dict } fn dict? { "Returns true if a value is a dict." (d as :dict) -> true (_) -> false } fn each! { "Takes a list and applies a function, presumably with side effects, to each element in the list. Returns nil." (f! as :fn, []) -> nil (f! as :fn, [x]) -> { f! (x); nil } (f! as :fn, [x, ...xs]) -> { f! (x); each! (f!, xs) } } &&& Trigonometry functions & Ludus uses turns as its default unit to measure angles & However, anything that takes an angle can also take a & units argument, that's a keyword of :turns, :degrees, or :radians let pi = base :pi let tau = mult (2, pi) fn sin { "Returns the sine of an angle. Default angle measure is turns. An optional keyword argument specifies the units of the angle passed in." (a as :number) -> do a > turn/rad > base :sin (a as :number, :turns) -> do a > turn/rad > base :sin (a as :number, :degrees) -> do a > deg/rad > base :sin (a as :number, :radians) -> base :sin (a) } fn cos { "Returns the cosine of an angle. Default angle measure is turns. An optional keyword argument specifies the units of the angle passed in." (a as :number) -> do a > turn/rad > base :cos (a as :number, :turns) -> do a > turn/rad > base :cos (a as :number, :degrees) -> do a > deg/rad > base :cos (a as :number, :radians) -> base :cos (a) } fn tan { "Returns the sine of an angle. Default angle measure is turns. An optional keyword argument specifies the units of the angle passed in." (a as :number) -> do a > turn/rad > base :tan (a as :number, :turns) -> do a > turn/rad > base :tan (a as :number, :degrees) -> do a > deg/rad > base :tan (a as :number, :radians) -> base :tan (a) } fn rotate { "Rotates a vector by an angle. Default angle measure is turns. An optional keyword argument specifies the units of the angle passed in." ((x, y), a) -> rotate ((x, y), a, :turns) ((x, y), a, units as :keyword) -> ( sub (mult (x, cos (a, units)), mult (y, sin (a, units))) add (mult (x, sin (a, units)), mult (y, cos (a, units))) ) } fn turn/deg { "Converts an angle in turns to an angle in degrees." (a as :number) -> mult (a, 360) } fn deg/turn { "Converts an angle in degrees to an angle in turns." (a as :number) -> div (a, 360) } fn turn/rad { "Converts an angle in turns to an angle in radians." (a as :number) -> mult (a, tau) } fn rad/turn { "Converts an angle in radians to an angle in turns." (a as :number) -> div (a, tau) } fn deg/rad { "Converts an angle in degrees to an angle in radians." (a as :number) -> mult (tau, div (a, 360)) } fn rad/deg { "Converts an angle in radians to an angle in degrees." (a as :number) -> mult (360, div (a, tau)) } fn atan/2 { "Returns an angle from a slope. Takes an optional keyword argument to specify units. Takes either two numbers or a vector tuple." (x as :number, y as :number) -> do base :atan_2 (x, y) > rad/turn (x, y, :turns) -> atan/2 (x, y) (x, y, :radians) -> base :atan_2 (x, y) (x, y, :degrees) -> do base :atan_2 (x, y) > rad/deg ((x, y)) -> atan/2 (x, y) ((x, y), units as :keyword) -> atan/2 (x, y, units) } fn mod { "Returns the modulus of x and y. Truncates towards negative infinity. Panics if y is 0." (x as :number, 0) -> panic! "Division by zero." (x as :number, y as :number) -> base :mod (x, y) } fn mod/0 { "Returns the modulus of x and y. Truncates towards negative infinity. Returns 0 if y is 0." (x as :number, 0) -> 0 (x as :number, y as :number) -> base :mod (x, y) } fn mod/safe { "Returns the modulus of x and y in a result tuple, or an error if y is 0. Truncates towards negative infinity." (x as :number, 0) -> (:err, "Division by zero.") (x as :number, y as :number) -> (:ok, base :mod (x, y)) } fn square { "Squares a number." (x as :number) -> mult (x, x) } fn sqrt { "Returns the square root of a number. Panics if the number is negative." (x as :number) if not (neg? (x)) -> base :sqrt (x) } fn sqrt/safe { "Returns a result containing the square root of a number, or an error if the number is negative." (x as :number) -> if not (neg? (x)) then (:ok, base :sqrt (x)) else (:err, "sqrt of negative number") } fn sum_of_squares { "Returns the sum of squares of numbers." () -> 0 (x as :number) -> square (x) (x as :number, y as :number) -> add (square (x), square (y)) (x, y, ...zs) -> fold ( fn (sum, z) -> add (sum, square (z)) zs sum_of_squares (x, y)) } fn dist { "Returns the distance from the origin to a point described by x and y, or by the vector (x, y)." (x as :number, y as :number) -> sqrt (sum_of_squares (x, y)) ((x, y)) -> dist (x, y) } fn heading/vector { "Takes a turtle heading, and returns a unit vector of that heading." (heading) -> { & 0 is 90º/0.25T, 0.25 is 180º/0.5T, 0.5 is 270º/0.75T, 0.75 is 0º/0T let a = add (neg (heading), 0.25) (cos (a), sin (a)) } } &&& more number functions fn random { "Returns a random something. With zero arguments, returns a random number between 0 (inclusive) and 1 (exclusive). With one argument, returns a random number between 0 and n. With two arguments, returns a random number between m and n. Alternately, given a collection (tuple, list, dict, set), it returns a random member of that collection." () -> base :random () (n as :number) -> mult (n, random ()) (m as :number, n as :number) -> add (m, random (sub (n, m))) (l as :list) -> { let i = do l > count > random > floor at (l, i) } (t as :tuple) -> { let i = do t > count > random > floor at (t, i) } (d as :dict) -> { let key = do d > keys > random get (key, d) } (s as :set) -> do s > list > random } fn random_int { "Returns a random integer. With one argument, returns a random integer between 0 and that number. With two arguments, returns a random integer between them." (n as :number) -> do n > random > floor (m as :number, n as :number) -> floor (random (m, n)) } fn floor { "Truncates a number towards negative infinity. With positive numbers, it returns the integer part. With negative numbers, returns the next more-negative integer." (n as :number) -> base :floor (n) } fn ceil { "Truncates a number towards positive infinity. With negative numbers, it returns the integer part. With positive numbers, returns the next more-positive integer." (n as :number) -> base :ceil (n) } fn round { "Rounds a number to the nearest integer." (n as :number) -> base :round (n) } fn range { "Returns the set of integers between start (inclusive) and end (exclusive) as a list: [start, end). With one argument, starts at 0. If end is less than start, returns an empty list." (end as :number) -> base :range (0, end) (start as :number, end as :number) -> base :range (start, end) } &&& Results, errors and other unhappy values fn ok { "Takes a value and wraps it in an :ok result tuple." (value) -> (:ok, value) } fn ok? { "Takes a value and returns true if it is an :ok result tuple." ((:ok, _)) -> true (_) -> false } fn err { "Takes a value and wraps it in an :err result tuple, presumably as an error message." (msg) -> (:err, msg) } fn err? { "Takes a value and returns true if it is an :err result tuple." ((:err, _)) -> true (_) -> false } fn unwrap! { "Takes a result tuple. If it's :ok, then returns the value. If it's not :ok, then it panics. If it's not a result tuple, it also panics." ((:ok, value)) -> value ((:err, msg)) -> panic! string ("Unwrapped :err! ", msg) (_) -> panic! "Cannot unwrap something that's not an error tuple." } fn unwrap_or { "Takes a value that is a result tuple and a default value. If it's :ok, then it returns the value. If it's :err, returns the default value." ((:ok, value), _) -> value ((:err, _), default) -> default } fn assert! { "Asserts a condition: returns the value if the value is truthy, panics if the value is falsy. Takes an optional message." (value) -> if value then value else panic! "Assert failed: {value}" (msg, value) -> if value then value else panic! "Assert failed: {msg} with {value}" } &&& Turtle & other graphics & some basic colors & these are the "basic" css colors & https://developer.mozilla.org/en-US/docs/Web/CSS/named-color let colors = #{ :black (0, 0, 0, 255) :silver (192, 192, 192, 255) :gray (128, 128, 128, 255) :white (255, 255, 255, 255) :maroon (128, 0, 0, 255) :red (255, 0, 0, 255) :purple (128, 0, 128, 255) :fuchsia (255, 0, 255, 255) :green (0, 128, 0, 255) :lime (0, 255, 0, 255) :olive (128, 128, 0, 255) :yellow (255, 255, 0, 255) :navy (0, 0, 128, 255) :blue (0, 0, 255, 255) :teal (0, 128, 128, 255) :aqua (0, 255, 25, 255) } & the initial turtle state let turtle_init = #{ :position (0, 0) & let's call this the origin for now :heading 0 & this is straight up :pendown? true :pencolor :white :penwidth 1 :visible? true } & turtle states: refs that get modified by calls & turtle_commands is a list of commands, expressed as tuples box turtle_commands = [] box turtle_state = turtle_init fn add_command! (command) -> { update! (turtle_commands, append! (_, command)) let prev = unbox (turtle_state) let curr = apply_command (prev, command) store! (turtle_state, curr) :ok } fn forward! { "Moves the turtle forward by a number of steps. Alias: fd!" (steps as :number) -> add_command! ((:forward, steps)) } let fd! = forward! fn back! { "Moves the turtle backward by a number of steps. Alias: bk!" (steps as :number) -> add_command! ((:back, steps)) } let bk! = back! fn left! { "Rotates the turtle left, measured in turns. Alias: lt!" (turns as :number) -> add_command! ((:left, turns)) } let lt! = left! fn right! { "Rotates the turtle right, measured in turns. Alias: rt!" (turns as :number) -> add_command! ((:right, turns)) } let rt! = right! fn penup! { "Lifts the turtle's pen, stopping it from drawing. Alias: pu!" () -> add_command! ((:penup)) } let pu! = penup! fn pendown! { "Lowers the turtle's pen, causing it to draw. Alias: pd!" () -> add_command! ((:pendown)) } let pd! = pendown! fn pencolor! { "Changes the turtle's pen color. Takes a single grayscale value, an rgb tuple, or an rgba tuple. Alias: pc!" (color as :keyword) -> add_command! ((:pencolor, color)) (gray as :number) -> add_command! ((:pencolor, (gray, gray, gray, 255))) ((r as :number, g as :number, b as :number)) -> add_command! ((:pencolor, (r, g, b, 255))) ((r as :number, g as :number, b as :number, a as :number)) -> add_command! ((:pencolor, (r, g, b, a))) } let pc! = pencolor! fn penwidth! { "Sets the width of the turtle's pen, measured in pixels. Alias: pw!" (width as :number) -> add_command! ((:penwidth, width)) } let pw! = penwidth! fn background! { "Sets the background color behind the turtle and path. Alias: bg!" (color as :keyword) -> add_command! ((:background, color)) (gray as :number) -> add_command! ((:background, (gray, gray, gray, 255))) ((r as :number, g as :number, b as :number)) -> add_command! ((:background, (r, g, b, 255))) ((r as :number, g as :number, b as :number, a as :number)) -> add_command! ((:background, (r, g, b, a))) } let bg! = background! fn home! { "Sends the turtle home: to the centre of the screen, pointing up. If the pen is down, the turtle will draw a path to home." () -> add_command! ((:home)) } fn clear! { "Clears the canvas and sends the turtle home." () -> add_command! ((:clear)) } fn goto! { "Sends the turtle to (x, y) coordinates. If the pen is down, the turtle will draw a path to its new location." (x as :number, y as :number) -> add_command! ((:goto, (x, y))) ((x, y)) -> goto! (x, y) } fn setheading! { "Sets the turtle's heading. The angle is specified in turns, with 0 pointing up. Increasing values rotate the turtle counter-clockwise." (heading as :number) -> add_command! ((:setheading, heading)) } fn showturtle! { "If the turtle is hidden, shows the turtle. If the turtle is already visible, does nothing." () -> add_command! ((:show)) } fn hideturtle! { "If the turtle is visible, hides it. If the turtle is already hidden, does nothing." () -> add_command! ((:hide)) } fn loadstate! { "Sets the turtle state to a previously saved state." (state) -> { let #{position, heading, pendown?, pencolor, penwidth, visible?} = state add_command! ((:loadstate, position, heading, visible?, pendown?, penwidth, pencolor)) } } fn apply_command { "Takes a turtle state and a command and calculates a new state." (state, command) -> match command with { (:goto, (x, y)) -> assoc (state, :position, (x, y)) (:home) -> do state > assoc (_, :position, (0, 0)) > assoc (_, :heading, 0) (:clear) -> do state > assoc (state, :position, (0, 0)) > assoc (_, :heading, 0) (:right, turns) -> update (state, :heading, add (_, turns)) (:left, turns) -> update (state, :heading, sub (_, turns)) (:forward, steps) -> { let #{heading, position, ...} = state let unit = heading/vector (heading) let vect = mult (steps, unit) update (state, :position, add (vect, _)) } (:back, steps) -> { let #{heading, position, ...} = state let unit = heading/vector (heading) let vect = mult (steps, unit) update (state, :position, sub (_, vect)) } (:penup) -> assoc (state, :pendown?, false) (:pendown) -> assoc (state, :pendown?, true) (:penwidth, pixels) -> assoc (state, :penwidth, pixels) (:pencolor, color) -> assoc (state, :pencolor, color) (:setheading, heading) -> assoc (state, :heading, heading) (:loadstate, position, heading, visible?, pendown?, penwidth, pencolor) -> #{position, heading, visible?, pendown?, penwidth, pencolor} (:show) -> assoc (state, :visible?, true) (:hide) -> assoc (state, :visible?, false) (:background, _) -> state } } & position () -> (x, y) fn position { "Returns the turtle's current position." () -> do turtle_state > unbox > :position } fn heading { "Returns the turtle's current heading." () -> do turtle_state > unbox > :heading } fn pendown? { "Returns the turtle's pen state: true if the pen is down." () -> do turtle_state > unbox > :pendown? } fn pencolor { "Returns the turtle's pen color as an (r, g, b, a) tuple or keyword." () -> do turtle_state > unbox > :pencolor } fn penwidth { "Returns the turtle's pen width in pixels." () -> do turtle_state > unbox > :penwidth } box state = nil pkg Prelude { abs & math add & math and & bool angle & math any? & dicts lists strings sets tuples append & lists sets assert! & errors assoc & dicts assoc? & dicts at & lists strings atan/2 & math back! & turtles background! & turtles between? & math bg! & turtles bk! & turtles bool & bool bool? & bool box? & boxes butlast & lists strings tuples ceil & math chars & strings clear! & turtles coll? & dicts lists sets tuples colors & turtles concat & string list set contains? & list set cos & math count & string list set tuple dict dec & math deg/rad & math deg/turn & math dict & dict dict? & dict diff & dict dissoc & dict dist & math div & math div/0 & math div/safe & math doc! & env downcase & string each! & list empty? & list dict set string tuple eq? & values err & result err? & result even? & math false? & bool fd! & turtles filter & list first & list tuple floor & math fn? & functions fold & lists forward! & turtles get & dicts goto! & turtles gt? & math gte? & math heading & turtles heading/vector & math hideturtle! & turtles home! & turtles inc & math inv & math inv/0 & math inv/safe & math join & lists strings keep & lists keys & dicts keyword? & keywords last & lists tuples left! & turtles list & lists list? & lists loadstate! & turtles lt! & turtles lt? & math lte? & math map & lists max & math min & math mod & math mod/0 mod/safe mult & math neg & math neg? & math neq? & values nil? & nil not & bool odd? & math ok & results ok? & results omit & set or & bool ordered? & lists tuples strings pc! & turtles pd! & turtles pencolor & turtles pencolor! & turtles pendown! & turtles pendown? & turtles penup! & turtles penwidth & turtles penwidth! & turtles pi & math pos? & math position & turtles print! & environment pu! & turtles pw! & turtles rad/deg & math rad/turn & math random & math dicts lists tuples sets random_int & math range & math lists report! & environment rest & lists tuples right! & turtles round & math rt! & turtles second & lists tuples sentence & lists strings set & sets set? & sets setheading! & turtles show & strings showturtle! & turtles sin & math slice & lists tuples strings some & values some? & values split & strings sqrt & math sqrt/safe & math square & math state & environment store! & boxes string & strings string? & strings strip & strings sub & math sum_of_squares & math tan & math tau & math to_number & strings numbers trim & strings tuple? & tuples turn/deg & math turn/rad & math turtle_commands & turtles turtle_init & turtles turtle_state & turtles type & values unbox & boxes unwrap! & results unwrap_or & results upcase & strings update & dicts update! & boxes values & dicts words & strings lists ws? & strings zero? & math }