Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sample function to List module, add log and exp functions to Float module #772

Merged
merged 7 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## v0.49.0 - 2024-12-19

- The `list` module gains the `sample` function.
- The `float` module gains the `exp` function.
- The `float` module gains the `log` function.
- The `list` module gains the `max` function.

## v0.48.0 - 2024-12-17
Expand Down
80 changes: 80 additions & 0 deletions src/gleam/float.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,83 @@ pub fn multiply(a: Float, b: Float) -> Float {
pub fn subtract(a: Float, b: Float) -> Float {
a -. b
}

/// Returns the natural logarithm (base e) of the given as a `Result`. If the
/// input is less than or equal to 0, returns `Error(Nil)`.
///
/// ## Examples
///
/// ```gleam
/// log(1.0)
/// // -> Ok(0.0)
/// ```
///
/// ```gleam
/// log(2.718281828459045) // e
/// // -> Ok(1.0)
/// ```
///
/// ```gleam
/// log(10.0)
/// // -> Ok(2.302585092994046)
/// ```
///
/// ```gleam
/// 100.0 |> log
/// // -> Ok(4.605170185988092)
/// ```
///
/// ```gleam
/// log(0.0)
/// // -> Error(Nil)
/// ```
///
/// ```gleam
/// log(-1.0)
/// // -> Error(Nil)
/// ```
///
pub fn log(x: Float) -> Result(Float, Nil) {
// In the following check:
// 1. If x is negative then return an error as the natural logarithm
// of a negative number is undefined (would be a complex number)
// 2. If x is 0 then return an error as the natural logarithm of 0
// approaches negative infinity
case x <=. 0.0 {
True -> Error(Nil)
False -> Ok(do_log(x))
}
}

@external(erlang, "math", "log")
@external(javascript, "../gleam_stdlib.mjs", "log")
fn do_log(x: Float) -> Float

/// Returns e (Euler's number) raised to the power of the given exponent, as
/// a `Float`.
///
/// ## Examples
///
/// ```gleam
/// exp(0.0)
/// // -> Ok(1.0)
/// ```
///
/// ```gleam
/// exp(1.0)
/// // -> Ok(2.718281828459045)
/// ```
///
/// ```gleam
/// exp(-1.0)
/// // -> Ok(0.36787944117144233)
/// ```
///
/// ```gleam
/// 2.0 |> exp
/// // -> Ok(7.38905609893065)
/// ```
///
@external(erlang, "math", "exp")
@external(javascript, "../gleam_stdlib.mjs", "exp")
pub fn exp(x: Float) -> Float
66 changes: 66 additions & 0 deletions src/gleam/list.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2337,3 +2337,69 @@ pub fn max(
}
})
}

/// Take a random sample of k elements from a list using reservoir sampling.
/// Returns an empty list if the sample size is less than or equal to 0.
///
/// Order is not random, only selection is.
///
/// ## Examples
///
/// ```gleam
/// reservoir_sample([1, 2, 3, 4, 5], 3)
/// // -> [2, 4, 5] // A random sample of 3 items
/// ```
///
pub fn sample(list: List(a), k: Int) -> List(a) {
case k <= 0 {
True -> []
False -> {
let #(reservoir, list) = split(list, k)

case length(reservoir) < k {
True -> reservoir
False -> {
let reservoir =
reservoir
|> map2(range(0, k - 1), _, fn(a, b) { #(a, b) })
|> dict.from_list

let w = float.exp(log_random() /. int.to_float(k))

do_sample(list, reservoir, k, k, w) |> dict.values
}
}
}
}
}

fn do_sample(
list,
reservoir: Dict(Int, a),
k,
index: Int,
w: Float,
) -> Dict(Int, a) {
let skip = {
let assert Ok(log_result) = float.log(1.0 -. w)

log_random() /. log_result |> float.floor |> float.round
}

let index = index + skip + 1

case drop(list, skip) {
[] -> reservoir
[elem, ..rest] -> {
let reservoir = int.random(k) |> dict.insert(reservoir, _, elem)
let w = w *. float.exp(log_random() /. int.to_float(k))

do_sample(rest, reservoir, k, index, w)
}
}
}

fn log_random() -> Float {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this function do? The name doesn't help me understand, so I'm not sure!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Algo L takes the log of a uniform random value but float.random is inclusive of 0. I need to add a small value to it as log(0) is undefined (a Result(Nil)) to assert its not an error. I can inline it if you prefer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use it in 3 places across sample and sample_loop so I thought it would be less noisy to extract it

let assert Ok(random) = float.log(float.random() +. 2.220446049250313e-16)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the significance of this number float?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need the result from float.random to be non-zero for log. I kind of forgot why that specific value, something related to float error. Should I use something like this value instead: rust f64::MIN_POSITIVE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched it to use the MIN_POSITIVE value

random
}
13 changes: 13 additions & 0 deletions src/gleam_stdlib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1010,3 +1010,16 @@ export function bit_array_starts_with(bits, prefix) {

return true;
}

export function log(x) {
// It is checked in Gleam that:
// - The input is strictly positive (x > 0)
// - This ensures that Math.log will never return NaN or -Infinity
// The function can thus safely pass the input to Math.log
// and a valid finite float will always be produced.
return Math.log(x);
}

export function exp(x) {
return Math.exp(x);
}
58 changes: 58 additions & 0 deletions test/gleam/float_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,61 @@ pub fn subtract_test() {
|> float.subtract(2.0, _)
|> should.equal(-1.0)
}

pub fn log_test() {
float.log(1.0)
|> should.equal(Ok(0.0))

float.log(2.718281828459045)
|> should.equal(Ok(1.0))

float.log(10.0)
|> should.equal(Ok(2.302585092994046))

float.log(100.0)
|> should.equal(Ok(4.605170185988092))

float.log(0.5)
|> should.equal(Ok(-0.6931471805599453))

float.log(0.1)
|> should.equal(Ok(-2.3025850929940455))

float.log(0.0)
|> should.equal(Error(Nil))

float.log(-1.0)
|> should.equal(Error(Nil))

float.log(-100.0)
|> should.equal(Error(Nil))

float.log(-0.1)
|> should.equal(Error(Nil))
}

pub fn exp_test() {
float.exp(0.0)
|> should.equal(1.0)

float.exp(1.0)
|> should.equal(2.718281828459045)

float.exp(2.0)
|> should.equal(7.38905609893065)

float.exp(-1.0)
|> should.equal(0.36787944117144233)

float.exp(5.0)
|> should.equal(148.4131591025766)

float.exp(-5.0)
|> should.equal(0.006737946999085467)

float.exp(0.000001)
|> should.equal(1.0000010000005)

float.exp(-100.0)
|> should.equal(3.720075976020836e-44)
}
49 changes: 49 additions & 0 deletions test/gleam/list_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -1299,3 +1299,52 @@ pub fn max_test() {
|> list.max(string.compare)
|> should.equal(Ok("c"))
}

pub fn sample_test() {
[]
|> list.sample(3)
|> should.equal([])

[1, 2, 3]
|> list.sample(0)
|> should.equal([])

[1, 2, 3]
|> list.sample(-1)
|> should.equal([])

[1, 2]
|> list.sample(5)
|> list.sort(int.compare)
|> should.equal([1, 2])

[1]
|> list.sample(1)
|> should.equal([1])

let input = list.range(1, 100)
let sample = list.sample(input, 10)
list.length(sample)
|> should.equal(10)

let repeated = [1, 1, 1, 1, 1]
let sample = list.sample(repeated, 3)
sample
|> list.all(fn(x) { x == 1 })
|> should.be_true()

let input = list.range(1, 1000)
let sample = list.sample(input, 100)
sample
|> list.sort(int.compare)
|> list.all(fn(x) { x >= 1 && x <= 1000 })
|> should.be_true()

list.length(sample)
|> should.equal(100)

let min = list.fold(sample, 1000, int.min)
let max = list.fold(sample, 1, int.max)
should.be_true(min >= 1)
should.be_true(max <= 1000)
}