Skip to content

Commit 9d76bea

Browse files
authored
Add sample function to List module, add log and exp functions to Float module (#772)
1 parent 9dee307 commit 9d76bea

File tree

6 files changed

+274
-1
lines changed

6 files changed

+274
-1
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## v0.49.0 - 2024-12-19
44

5-
- The `list` module gains the `max` function.
5+
- The `float` module gains the `exponential` and `logarithm` function.
6+
- The `list` module gains the `max` and `sample` function.
67

78
## v0.48.0 - 2024-12-17
89

src/gleam/float.gleam

+65
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,68 @@ pub fn multiply(a: Float, b: Float) -> Float {
572572
pub fn subtract(a: Float, b: Float) -> Float {
573573
a -. b
574574
}
575+
576+
/// Returns the natural logarithm (base e) of the given as a `Result`. If the
577+
/// input is less than or equal to 0, returns `Error(Nil)`.
578+
///
579+
/// ## Examples
580+
///
581+
/// ```gleam
582+
/// logarithm(1.0)
583+
/// // -> Ok(0.0)
584+
/// ```
585+
///
586+
/// ```gleam
587+
/// logarithm(2.718281828459045) // e
588+
/// // -> Ok(1.0)
589+
/// ```
590+
///
591+
/// ```gleam
592+
/// logarithm(0.0)
593+
/// // -> Error(Nil)
594+
/// ```
595+
///
596+
/// ```gleam
597+
/// logarithm(-1.0)
598+
/// // -> Error(Nil)
599+
/// ```
600+
///
601+
pub fn logarithm(x: Float) -> Result(Float, Nil) {
602+
// In the following check:
603+
// 1. If x is negative then return an error as the natural logarithm
604+
// of a negative number is undefined (would be a complex number)
605+
// 2. If x is 0 then return an error as the natural logarithm of 0
606+
// approaches negative infinity
607+
case x <=. 0.0 {
608+
True -> Error(Nil)
609+
False -> Ok(do_log(x))
610+
}
611+
}
612+
613+
@external(erlang, "math", "log")
614+
@external(javascript, "../gleam_stdlib.mjs", "log")
615+
fn do_log(x: Float) -> Float
616+
617+
/// Returns e (Euler's number) raised to the power of the given exponent, as
618+
/// a `Float`.
619+
///
620+
/// ## Examples
621+
///
622+
/// ```gleam
623+
/// exponential(0.0)
624+
/// // -> Ok(1.0)
625+
/// ```
626+
///
627+
/// ```gleam
628+
/// exponential(1.0)
629+
/// // -> Ok(2.718281828459045)
630+
/// ```
631+
///
632+
/// ```gleam
633+
/// exponential(-1.0)
634+
/// // -> Ok(0.36787944117144233)
635+
/// ```
636+
///
637+
@external(erlang, "math", "exp")
638+
@external(javascript, "../gleam_stdlib.mjs", "exp")
639+
pub fn exponential(x: Float) -> Float

src/gleam/list.gleam

+67
Original file line numberDiff line numberDiff line change
@@ -2337,3 +2337,70 @@ pub fn max(
23372337
}
23382338
})
23392339
}
2340+
2341+
/// Take a random sample of k elements from a list using reservoir sampling via
2342+
/// Algo L. Returns an empty list if the sample size is less than or equal to 0.
2343+
///
2344+
/// Order is not random, only selection is.
2345+
///
2346+
/// ## Examples
2347+
///
2348+
/// ```gleam
2349+
/// reservoir_sample([1, 2, 3, 4, 5], 3)
2350+
/// // -> [2, 4, 5] // A random sample of 3 items
2351+
/// ```
2352+
///
2353+
pub fn sample(list: List(a), k: Int) -> List(a) {
2354+
case k <= 0 {
2355+
True -> []
2356+
False -> {
2357+
let #(reservoir, list) = split(list, k)
2358+
2359+
case length(reservoir) < k {
2360+
True -> reservoir
2361+
False -> {
2362+
let reservoir =
2363+
reservoir
2364+
|> map2(range(0, k - 1), _, fn(a, b) { #(a, b) })
2365+
|> dict.from_list
2366+
2367+
let w = float.exponential(log_random() /. int.to_float(k))
2368+
2369+
sample_loop(list, reservoir, k, k, w) |> dict.values
2370+
}
2371+
}
2372+
}
2373+
}
2374+
}
2375+
2376+
fn sample_loop(
2377+
list,
2378+
reservoir: Dict(Int, a),
2379+
k,
2380+
index: Int,
2381+
w: Float,
2382+
) -> Dict(Int, a) {
2383+
let skip = {
2384+
let assert Ok(log_result) = float.logarithm(1.0 -. w)
2385+
2386+
log_random() /. log_result |> float.floor |> float.round
2387+
}
2388+
2389+
let index = index + skip + 1
2390+
2391+
case drop(list, skip) {
2392+
[] -> reservoir
2393+
[elem, ..rest] -> {
2394+
let reservoir = int.random(k) |> dict.insert(reservoir, _, elem)
2395+
let w = w *. float.exponential(log_random() /. int.to_float(k))
2396+
2397+
sample_loop(rest, reservoir, k, index, w)
2398+
}
2399+
}
2400+
}
2401+
2402+
fn log_random() -> Float {
2403+
let min_positive = 2.2250738585072014e-308
2404+
let assert Ok(random) = float.logarithm(float.random() +. min_positive)
2405+
random
2406+
}

src/gleam_stdlib.mjs

+13
Original file line numberDiff line numberDiff line change
@@ -1010,3 +1010,16 @@ export function bit_array_starts_with(bits, prefix) {
10101010

10111011
return true;
10121012
}
1013+
1014+
export function log(x) {
1015+
// It is checked in Gleam that:
1016+
// - The input is strictly positive (x > 0)
1017+
// - This ensures that Math.log will never return NaN or -Infinity
1018+
// The function can thus safely pass the input to Math.log
1019+
// and a valid finite float will always be produced.
1020+
return Math.log(x);
1021+
}
1022+
1023+
export function exp(x) {
1024+
return Math.exp(x);
1025+
}

test/gleam/float_test.gleam

+78
Original file line numberDiff line numberDiff line change
@@ -528,3 +528,81 @@ pub fn subtract_test() {
528528
|> float.subtract(2.0, _)
529529
|> should.equal(-1.0)
530530
}
531+
532+
pub fn logarithm_test() {
533+
float.logarithm(1.0)
534+
|> result.unwrap(or: 1.0)
535+
|> float.loosely_equals(with: 0.0, tolerating: 0.001)
536+
|> should.be_true
537+
538+
float.logarithm(2.718281828459045)
539+
|> result.unwrap(or: 0.0)
540+
|> float.loosely_equals(with: 1.0, tolerating: 0.001)
541+
|> should.be_true
542+
543+
float.logarithm(10.0)
544+
|> result.unwrap(or: 0.0)
545+
|> float.loosely_equals(with: 2.302585092994046, tolerating: 0.001)
546+
|> should.be_true
547+
548+
float.logarithm(100.0)
549+
|> result.unwrap(or: 0.0)
550+
|> float.loosely_equals(with: 4.605170185988092, tolerating: 0.001)
551+
|> should.be_true
552+
553+
float.logarithm(0.5)
554+
|> result.unwrap(or: 0.0)
555+
|> float.loosely_equals(with: -0.6931471805599453, tolerating: 0.001)
556+
|> should.be_true
557+
558+
float.logarithm(0.1)
559+
|> result.unwrap(or: 0.0)
560+
|> float.loosely_equals(with: -2.3025850929940455, tolerating: 0.001)
561+
|> should.be_true
562+
563+
float.logarithm(0.0)
564+
|> should.equal(Error(Nil))
565+
566+
float.logarithm(-1.0)
567+
|> should.equal(Error(Nil))
568+
569+
float.logarithm(-100.0)
570+
|> should.equal(Error(Nil))
571+
572+
float.logarithm(-0.1)
573+
|> should.equal(Error(Nil))
574+
}
575+
576+
pub fn exponential_test() {
577+
float.exponential(0.0)
578+
|> float.loosely_equals(with: 1.0, tolerating: 0.001)
579+
|> should.be_true
580+
581+
float.exponential(1.0)
582+
|> float.loosely_equals(with: 2.718281828459045, tolerating: 0.001)
583+
|> should.be_true
584+
585+
float.exponential(2.0)
586+
|> float.loosely_equals(with: 7.38905609893065, tolerating: 0.001)
587+
|> should.be_true
588+
589+
float.exponential(-1.0)
590+
|> float.loosely_equals(with: 0.36787944117144233, tolerating: 0.001)
591+
|> should.be_true
592+
593+
float.exponential(5.0)
594+
|> float.loosely_equals(with: 148.4131591025766, tolerating: 0.001)
595+
|> should.be_true
596+
597+
float.exponential(-5.0)
598+
|> float.loosely_equals(with: 0.006737946999085467, tolerating: 0.001)
599+
|> should.be_true
600+
601+
float.exponential(0.000001)
602+
|> float.loosely_equals(with: 1.0000010000005, tolerating: 0.001)
603+
|> should.be_true
604+
605+
float.exponential(-100.0)
606+
|> float.loosely_equals(with: 3.720075976020836e-44, tolerating: 0.001)
607+
|> should.be_true
608+
}

test/gleam/list_test.gleam

+49
Original file line numberDiff line numberDiff line change
@@ -1299,3 +1299,52 @@ pub fn max_test() {
12991299
|> list.max(string.compare)
13001300
|> should.equal(Ok("c"))
13011301
}
1302+
1303+
pub fn sample_test() {
1304+
[]
1305+
|> list.sample(3)
1306+
|> should.equal([])
1307+
1308+
[1, 2, 3]
1309+
|> list.sample(0)
1310+
|> should.equal([])
1311+
1312+
[1, 2, 3]
1313+
|> list.sample(-1)
1314+
|> should.equal([])
1315+
1316+
[1, 2]
1317+
|> list.sample(5)
1318+
|> list.sort(int.compare)
1319+
|> should.equal([1, 2])
1320+
1321+
[1]
1322+
|> list.sample(1)
1323+
|> should.equal([1])
1324+
1325+
let input = list.range(1, 100)
1326+
let sample = list.sample(input, 10)
1327+
list.length(sample)
1328+
|> should.equal(10)
1329+
1330+
let repeated = [1, 1, 1, 1, 1]
1331+
let sample = list.sample(repeated, 3)
1332+
sample
1333+
|> list.all(fn(x) { x == 1 })
1334+
|> should.be_true()
1335+
1336+
let input = list.range(1, 1000)
1337+
let sample = list.sample(input, 100)
1338+
sample
1339+
|> list.sort(int.compare)
1340+
|> list.all(fn(x) { x >= 1 && x <= 1000 })
1341+
|> should.be_true()
1342+
1343+
list.length(sample)
1344+
|> should.equal(100)
1345+
1346+
let min = list.fold(sample, 1000, int.min)
1347+
let max = list.fold(sample, 1, int.max)
1348+
should.be_true(min >= 1)
1349+
should.be_true(max <= 1000)
1350+
}

0 commit comments

Comments
 (0)