diff --git a/.typos.toml b/.typos.toml index 4312784a4..983192d14 100644 --- a/.typos.toml +++ b/.typos.toml @@ -4,6 +4,7 @@ Hashi = "Hashi" # CLI help abbreviation syntax: b[reakpoint], l[ist] reakpoint = "reakpoint" ist = "ist" +juxt = "juxt" [default.extend-identifiers] # bimap is a crate name (bijective map) @@ -15,3 +16,4 @@ caf = "caf" abd = "abd" hel = "hel" fo = "fo" + diff --git a/crates/mq-lang/builtin.mq b/crates/mq-lang/builtin.mq index bb6d84042..63017635a 100644 --- a/crates/mq-lang/builtin.mq +++ b/crates/mq-lang/builtin.mq @@ -367,6 +367,23 @@ def group_by(arr, f): end end +# Counts occurrences of each key extracted from the elements of an array, +# returning a dict of `{key: count}`. +def frequencies_by(arr, f): + if (not(is_array(arr))): + error("first argument must be an array") + else: + fold(arr, dict(), fn(acc, x): + let key = to_string(f(x)) + | let existing = get(acc, key) + | let count = if (is_none(existing)): 1 else: existing + 1 + | set(acc, key, count); + ) +end + +# Counts occurrences of each element in an array, returning a dict of `{value: count}`. +def tally(arr): frequencies_by(arr, identity); + # Returns true if any element in the array satisfies the provided function. def any(v, f): if (is_empty(v)): false else: len(filter(v, f)) != 0; @@ -792,6 +809,17 @@ end # Returns a new function that takes the same arguments as the original function but with the first two arguments flipped. def flip(f, a, b): f(b, a); +# Returns a new predicate function that negates the result of the given function. +def complement(f): fn(x): !f(x);; + +# Composes functions into one function, applying them right-to-left. +# `comp(f, g, h)(x)` is equivalent to `f(g(h(x)))`. +def comp(*fns): fn(x): fold(reverse(fns), x, fn(acc, f): f(acc););; + +# Returns a function that applies each given function to its argument and collects the results into an array. +# `juxt(f, g, h)(x)` is equivalent to `[f(x), g(x), h(x)]`. +def juxt(*fns): fn(x): map(fns, fn(f): f(x););; + # Returns the sum of the elements in an array after applying a transformation function to each element. def sum(arr): sum_by(arr, identity); diff --git a/crates/mq-lang/builtin_tests.mq b/crates/mq-lang/builtin_tests.mq index 98821bbd6..4af43cd4b 100644 --- a/crates/mq-lang/builtin_tests.mq +++ b/crates/mq-lang/builtin_tests.mq @@ -384,6 +384,25 @@ def test_group_by(): | assert_eq(result2, dict()) end +def test_frequencies_by(): + let result1 = frequencies_by([1, 2, 3, 4, 5, 6], fn(x): x % 2;) + | assert_eq(result1["0"], 3) + | assert_eq(result1["1"], 3) + + | let result2 = frequencies_by([], fn(x): x;) + | assert_eq(result2, dict()) +end + +def test_tally(): + let result1 = tally(["a", "b", "a", "c", "b", "a"]) + | assert_eq(result1["a"], 3) + | assert_eq(result1["b"], 2) + | assert_eq(result1["c"], 1) + + | let result2 = tally([]) + | assert_eq(result2, dict()) +end + def test_count_by(): let result1 = count_by([1, 2, 3, 4, 5], fn(x): x > 2;) | assert_eq(result1, 3) @@ -767,6 +786,37 @@ def test_flip(): | assert_eq(result2, -3) end +def test_complement(): + let is_even = fn(x): x % 2 == 0; + | let is_odd = complement(is_even) + | assert_eq(is_odd(3), true) + | assert_eq(is_odd(4), false) + | assert_eq(filter([1, 2, 3, 4, 5], complement(is_even)), [1, 3, 5]) +end + +def test_comp(): + let inc = fn(x): x + 1; + | let double = fn(x): x * 2; + | let f = comp(inc, double) + | assert_eq(f(5), 11) + + | let g = comp(double, inc) + | assert_eq(g(5), 12) + + | let h = comp() + | assert_eq(h(5), 5) +end + +def test_juxt(): + let inc = fn(x): x + 1; + | let double = fn(x): x * 2; + | let f = juxt(inc, double) + | assert_eq(f(5), [6, 10]) + + | let g = juxt() + | assert_eq(g(5), []) +end + def test_sum(): let result1 = sum([1, 2, 3, 4, 5]) | assert_eq(result1, 15)