So, this manner of reintroducing something like object-oriented programming seems obviously un-Gleamish to me:
import gleam/float
type PrecisionArithmetic {
PrecisionArithmetic(
add: fn(Float, Float) -> Float,
sub: fn(Float, Float) -> Float,
mul: fn(Float, Float) -> Float,
div: fn(Float, Float) -> Float,
change_precision: fn(Int) -> PrecisionArithmetic,
)
}
fn with_precision(n: Int) -> PrecisionArithmetic {
PrecisionArithmetic(
add: rounded_op(fn(a, b) { a +. b }, n),
sub: rounded_op(fn(a, b) { a -. b }, n),
mul: rounded_op(fn(a, b) { a *. b }, n),
div: rounded_op(fn(a, b) { a /. b }, n),
change_precision: fn(d) { with_precision(n + d) },
)
}
fn rounded_op(op: fn(Float, Float) -> Float, n: Int) {
fn(lt, rt) { op(lt, rt) |> float.to_precision(n) }
}
fn show(v) {
echo v
v
}
pub fn main() {
let ops = with_precision(3)
ops.add(123.456, 100.1) |> show // -> 223.556
|> ops.mul(0.1) |> show // -> 22.356
let ops = ops.change_precision(-2)
ops.sub(123.456, 0.0001) |> show // -> 123.5
|> ops.div(10.0) |> show // -> 12.4
}
This code is obviously a bit silly (maybe itself a warning...) and I mean it just as an example of the pattern--but can anyone put words to the intuition here? What is it about parameterizing a set of functions, possibly with some closed-over state accessible only to those functions, that we want to avoid?
In this example it is a bit glaring how the change_precision function clashes with the pipe, and of course part of the point of the pipe operator is to allow us to emulate the object-oriented style but with looser coupling between functions. But I've also found myself writing modules where every function takes a custom type as the first parameter, possibly with additional customizing parameters common between functions, which starts to feel a bit silly in its own way.
Thoughts?