& this file, uniquely, gets `base` loaded as context. See src/ludus/base.cljc for exports &&& TODO & functions to add: & true? & set? & tuple? & ref? & some forward declarations & TODO: fix this so that we don't need (as many of) them fn first fn append fn some? fn update! fn string fn join fn neg? fn atan/2 fn mod fn assoc? fn dict fn get fn unbox fn store! fn turn/rad fn deg/rad fn floor fn and fn apply_command fn state/call & the very base: know something's type fn type { "Returns a keyword representing the type of the value passed in." (x) -> base :type (x) } & ...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) } 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) -> dec (base :count (xs)) (xs as :tuple) -> dec (base :count (xs)) (xs as :dict) -> base :count (xs) (xs as :string) -> base :count (xs) (xs as :set) -> base :count (xs) (xs as :struct) -> dec (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. Associative collections return lists of (key, value) tuples." (x) -> base :to_list (x) } & TODO: make this work with Janet base fn set { "Takes an ordered collection--list or tuple--and turns it into a set." (xs as :list) -> base :into (${}, xs) (xs as :tuple) -> base :into (${}, xs) } fn set? { "Returns true if a value is a set." (xs as :set) -> true (_) -> false } & add a contains? or has? function fn fold { "Folds a list." (f as :fn, xs as :list) -> fold (f, xs, f ()) (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 concat { "Combines two lists, strings, or sets." (x as :string, y as :string) -> base :str (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)) } & the console: sending messages to the outside world & the console is *both* something we send to the host language's console & ...and also a list of messages. box console = [] fn flush! { "Clears the console, and returns the messages." () -> { let msgs = unbox (console) store! (console, []) msgs } } fn add_msg! { "Adds a message to the console." (msg as :string) -> update! (console, append (_, msg)) (msgs as :list) -> { let msg = do msgs > map (string, _) > join update! (console, append (_, msg)) } } fn print! { "Sends a text representation of Ludus values to the console." (...args) -> { base :print (args) add_msg! (args) :ok } } fn show { "Returns a text representation of a Ludus value as a string." (x) -> base :show (x) } fn prn! { "Prints the underlying Clojure data structure of a Ludus value." (x) -> base :prn (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 differen kinds of values." (x as :string) -> x (x) -> show (x) (x, ...xs) -> loop (x, xs) with { (out, [x]) -> concat (out, show(x)) (out, [x, ...xs]) -> recur (concat (out, show (x)), xs) } } 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, "") ([str, ...strs], separator as :string) -> fold ( fn (joined, to_join) -> concat (joined, separator, to_join) strs str ) } & in another prelude, with a better actual base language than Java (thanks, Rich), counting strings would be reasonable but complex: count/bytes, count/points, count/glyphs. Java's UTF16 strings make this unweildy. & TODO: add trim, trim/left, trim/right; pad/left, pad/right & ...also a version of at, &&& references: 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 :set! (b, 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 (base :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 (base :sub, zs, base :sub (x, y)) ((x1, y1), (x2, y2)) -> (base :sub (x1, x2), base :sub (x2, 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 (base :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 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. Zero-indexed: the first element is at index 0." (xs as :list, n as :number) -> when { neg? (n) -> nil gte? (n, count (xs)) -> nil true -> base :nth (xs, inc (n)) } (xs as :tuple, n as :number) -> when { neg? (n) -> nil gte? (n, count (xs)) -> nil true -> base :nth (xs, inc (n)) } (_) -> nil } fn first { "Returns the first element of a list or tuple." (xs) -> at (xs, 0) } fn second { "Returns the second element of a list or tuple." (xs) -> at (xs, 1) } fn last { "Returns the last element of a list or tuple." (xs) -> at (xs, dec (count (xs))) } fn butlast { "Returns a list, omitting the last element." (xs as :list) -> base :slice (xs, dec (count (xs))) } fn slice { "Returns a slice of a list, representing a sub-list." (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 -> { let slice = base :slice (xs, inc (start), inc (end)) base :into ([], slice) } } } &&& 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. &&& 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 } & 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 (base :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 (base :or, zs, base :or (x, y)) } &&& associative collections: dicts, structs, namespaces & TODO?: get_in, update_in, merge & TODO?: consider renaming these: put & take, not assoc/dissoc fn assoc { "Takes a dict, key, and value, and returns a new dict with the key set to value." () -> #{} (dict as :dict) -> dict (dict as :dict, key as :keyword, value) -> base :assoc (dict, key, value) (dict as :dict, (key as :keyword, value)) -> base :assoc (dict, key, value) } fn dissoc { "Takes a dict and a key, and returns a new dict with the key and associated value omitted." (dict as :dict) -> dict (dict as :dict, key as :keyword) -> base :dissoc (dict, key) } 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." (dict as :dict) -> dict (dict as :dict, key as :keyword, updater as :fn) -> base :assoc (dict, key, updater (get (key, dict))) } fn keys { "Takes an associative collection and returns a list of keys in that collection. Returns an empty list on anything other than a collection." (coll) -> if not (assoc? (coll)) then [] else do coll > list > map (first, _) } fn values { "Takes an associative collection and returns a list of values in that collection. Returns an empty list on anything other than a collection." (coll) -> if not (assoc? (coll)) then [] else do coll > list > map (second, _) } fn diff { "Takes two associate data structures 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) } } fn coll? { "Returns true if a value is a collection: dict, struct, list, tuple, or set." (coll as :dict) -> true (coll as :list) -> true (coll as :tuple) -> true (coll as :set) -> true (coll as :ns) -> true (_) -> false } fn ordered? { "Returns true if a value is an indexed collection: list or tuple." (coll as :list) -> true (coll as :tuple) -> true (_) -> false } fn assoc? { "Returns true if a value is an associative collection: a dict, struct, or namespace." (assoc as :dict) -> true (assoc as :struct) -> true (assoc as :ns) -> true (_) -> false } & TODO: consider merging `get` and `at` fn get { "Takes a dict or struct, key, and optional default value; returns the value at key. If the value is not found, returns nil or the default value. Returns nil or default if the first argument is not a dict or struct." (key as :keyword) -> get (key, _) (key as :keyword, coll) -> base :get (key, coll) (key as :keyword, coll, default) -> base :get (key, coll, 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." (key as :keyword) -> has? (key, _) (key as :keyword, dict as :dict) -> do dict > key > nil? } fn dict { "Takes an ns, and returns it as a dict. Or, takes a list or tuple of (key, value) tuples and returns it as a dict. Returns dicts unharmed." (ns_ as :ns) -> base :to_dict (ns_) (dict as :dict) -> dict (list as :list) -> fold (assoc, list) (tup as :tuple) -> do tup > list > dict } fn dict? { "Returns true if a value is a dict." (dict as :dict) -> true (_) -> false } & TODO: make this less awkward once we have tail recursion 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, [...xs]) -> loop (xs) with { ([x]) -> { f! (x); nil } ([x, ...xs]) -> { f! (x); recur (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), angle) -> rotate ((x, y), angle, :turns) ((x, y), angle, units as :keyword) -> ( sub (mult (x, cos (angle, units)), mult (y, sin (angle, units))) add (mult (x, sin (angle, units)), mult (y, cos (angle, 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 num and div. Truncates towards negative infinity." (num as :number, y as :number) -> base :mod (num, div) } fn square { "Squares a number." (x as :number) -> mult (x, x) } fn sqrt { "Returns the square root of a number." (x as :number) -> base :sqrt (x) } 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 (sum_of_squares, zs, sum_of_squares (x, y)) } fn dist { "Returns the distance from the origin to a point described by (x, y)." (x as :number, y as :number) -> sqrt (sum_of_squares (x, y)) ((x, y)) -> dist (x, y) } &&& more number functions fn random { "Returns a random number. 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 list, it returns a random member of that list." () -> base :random () (n as :number) -> base :random (n) (m as :number, n as :number) -> add (m, random (n)) (l as :list) -> { let i = do l > count > random > floor at (l, i) } (d as :dict) -> { let key = do d > keys > random get (d, key) } } 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! string ("Assert failed:", value) (msg, value) -> if value then value else panic! string ("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 :color colors :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 = [] & and a list of turtle states box turtle_states = [turtle_init] fn reset_turtle! { "Resets the turtle to its original state." () -> store! (turtle_states, [turtle_init]) } & and a list of calls to p5--at least for now box p5_calls = [] & ...and finally, a background color & we need to store this separately because, while it can be updated later, & it must be the first call to p5. box bgcolor = colors :black fn add_call! (call) -> update! (p5_calls, append (_, call)) fn add_command! (command) -> { update! (turtle_commands, append (_, command)) let prev = do turtle_states > unbox > last let curr = apply_command (prev, command) update! (turtle_states, append (_, curr)) let call = state/call () if call then { add_call! (call); :ok } else :ok } fn make_line ((x1, y1), (x2, y2)) -> (:line, x1, y1, x2, y2) let turtle_radius = 20 let turtle_angle = 0.385 let turtle_color = (255, 255, 255, 150) fn render_turtle! () -> { let state = do turtle_states > unbox > last if state :visible? then { let (r, g, b, a) = turtle_color add_call! ((:fill, r, g, b, a)) let #{heading, :position (x, y)} = state let first = mult ((0, 1), turtle_radius) let (x1, y1) = first let (x2, y2) = rotate (first, turtle_angle) let (x3, y3) = rotate (first, neg (turtle_angle)) add_call! ((:push)) add_call! ((:translate, x, y)) add_call! ((:rotate, turn/rad (heading))) add_call! ((:noStroke)) add_call! ((:beginShape)) add_call! ((:vertex, x1, y1)) add_call! ((:vertex, x2, y2)) add_call! ((:vertex, x3, y3)) add_call! ((:endShape)) & there's a happy bug here: the stroke will be the same width as the pen width. Keep this for now. Consider also showing the pen colour here? add_call! ((:stroke, 0)) add_call! ((:line, 0, 0, x1, y1)) add_call! ((:pop)) :ok } else :ok } fn state/call () -> { let cmd = do turtle_commands > unbox > last > first let states = unbox (turtle_states) let curr = last (states) let prev = at (states, sub (count (states), 2)) match cmd with { :forward -> if curr :pendown? then make_line (prev :position, curr :position) else nil :back -> if curr :pendown? then make_line (prev :position, curr :position) else nil :home -> if curr :pendown? then make_line (prev :position, curr :position) else nil :goto -> if curr :pendown? then make_line (prev :position, curr :position) else nil :penwidth -> (:strokeWeight, curr :penwidth) :pencolor -> { let (r, g, b, a) = curr :pencolor (:stroke, r, g, b, a) } :clear -> (:background, 0, 0, 0, 255) _ -> nil } } 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! & turtles, like eveyrthing else in Ludus, use turns by default, & not degrees 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!" (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!" (gray as :number) -> store! (bgcolor, (gray, gray, gray, 255)) ((r as :number, g as :number, b as :number)) -> store! (bgcolor, (r, b, g, 255)) ((r as :number, g as :number, b as :number, a as :number)) -> store! (bgcolor, (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 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 angle = add (heading, 0.25) (cos (angle), sin (angle)) } } 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) -> assoc (state, :position, (0, 0)) (:clear) -> assoc (state, :position, (0, 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) } } fn turtle_state { "Returns the turtle's current state." () -> do turtle_states > unbox > last } & position () -> (x, y) fn position { "Returns the turtle's current position." () -> turtle_state () :position } fn heading { "Returns the turtle's current heading." () -> turtle_state () :heading } fn pendown? { "Returns the turtle's pen state: true if the pen is down." () -> turtle_state () :pendown? } fn pencolor { "Returns the turtle's pen color as an (r, g, b, a) tuple." () -> turtle_state () :pencolor } fn penwidth { "Returns the turtle's pen width in pixels." () -> turtle_state () :pencolor } pkg Prelude { type eq? neq? tuple? fn? empty? any? first second rest at last butlast slice count append fold map filter keep list set set? inc dec print! flush! console show prn! report! doc! concat box? unbox store! update! string string? join add sub mult div div/0 div/safe inv inv/0 angle abs neg zero? neg? pos? even? odd? gt? gte? lt? lte? min max between? keyword? nil? some? some bool? false? bool not and or coll? ordered? assoc? assoc dissoc update get dict dict? keys values diff each! sin cos tan turn/rad rad/turn turn/deg deg/turn rad/deg deg/rad atan/2 mod square sum_of_squares dist random random_int pi tau floor ceil round range ok ok? err err? unwrap! unwrap_or assert! colors forward!, fd! back!, bk! right!, rt! left!, lt! penup!, pu! pendown!, pd! pencolor!, pc! background!, bg! penwidth!, pw! home!, clear!, goto!, heading, position, pendown? pencolor, penwidth heading/vector turtle_state p5_calls, turtle_states, turtle_commands, bgcolor render_turtle!, reset_turtle! }