I’ve been doing Elixir for 9 years, 5 professionally. Nobody cares about ad-hoc polymorphism. The community doesn’t use protocols except “for data”. Whatever that means. Global singleton processes everywhere. I’m really discouraged by the practices I observe but it’s the most enjoyable language for me still.
>I’ve been doing Elixir for 9 years, 5 professionally. Nobody cares about ad-hoc polymorphism.
That’s true for Elixir as practiced, but it’s the wrong conclusion for Gleam.
Elixir doesn’t care about ad-hoc polymorphism because in Elixir it’s a runtime convention, not a compile-time guarantee. Protocols don’t give you universal quantification, exhaustiveness, coherence, or refactoring safety. Missing cases become production crashes, not compiler errors. So teams sensibly avoid building architecture on top of them.
In a statically typed language, ad-hoc polymorphism is a different beast entirely. It’s one of the primary ways you encode abstraction safely. The compiler enforces that implementations exist, pushes back on missing cases, and lets you refactor without widening everything into explicit pattern matches.
That’s exactly why people who like static types do care about it.
Pointing to Elixir community norms and concluding “nobody cares” is mixing up ecosystem habits with language design. Elixir doesn’t reward those abstractions, so people don’t use them. Gleam is explicitly targeting people who want the compiler to carry more of the burden.
If Gleam is “Elixir with types,” fine, lack of ad-hoc polymorphism is consistent.
If it’s “a serious statically typed language on the BEAM,” then the absence is a real limitation, not bikeshedding.
Static types aren’t about catching typos. They’re about moving failure from runtime to compile time. Ad-hoc polymorphism is one of the main tools for doing that without collapsing everything into concrete types.
That’s why the criticism exists, regardless of how Elixir codebases look today.
It's an education problem on two fronts. People inside the ecosystem need to know about it. And also people too deep in the elixir ecosystem who don't know how ad-hoc polymorphism is supposed to be used in a statically typed language.
Both overcome it by admitting they don't know and need to learn.
If we embraced REST, as Roy Fielding envisioned it, we wouldn't have this, and all similar, conversations. REST doesn't expose identifier, it only exposes relationships. Identifiers are an implementation details.
I'm thinking of using C++ for a personal project specifically for the lambdas and RAII.
I have a case where I need to create a static templated lambda to be passed to C as a pointer. Such thing is impossible in Rust, which I considered at first.
Yeah, Rust closures that capture data are fat pointers { fn*, data* }, so you need an awkward dance to make them thin pointers for C.
let mut state = 1;
let mut fat_closure = || state += 1;
let (fnptr, userdata) = make_trampoline(&mut &mut fat_closure);
unsafe {
fnptr(userdata);
}
assert_eq!(state, 2);
use std::ffi::c_void;
fn make_trampoline<C: FnMut()>(closure: &mut &mut C) -> (unsafe fn(*mut c_void), *mut c_void) {
let fnptr = |userdata: *mut c_void| {
let closure: *mut &mut C = userdata.cast();
(unsafe { &mut *closure })()
};
(fnptr, closure as *mut _ as *mut c_void)
}
It requires a userdata arg for the C function, since there's no allocation or executable-stack magic to give a unique function pointer to each data instance. OTOH it's zero-cost. The generic make_trampoline inlines code of the closure, so there's no extra indirection.
> Rust closures that capture data are fat pointers { fn, data }
This isn’t fully accurate. In your example, `&mut C` actually has the same layout as usize. It’s not a fat pointer. `C` is a concrete type and essentially just an anonymous struct with FnMut implemented for it.
You’re probably thinking of `&mut dyn FnMut` which is a fat pointer that pairs a pointer to the data with a pointer to a VTable.
So in your specific example, the double indirection is unnecessary.
This is a problem for all capturing closures though, not just Rust's. A pure fn-ptr arg can't have state, and if there's no user data arg then there's no way to make a trampoline. If C++ was calling a C API with the same constraint it would have the same problem.
Well, capturing closures that are implemented like C++ lambdas or Rust closures anyway. The executable stack crimes do make a thin fn-ptr with state.
If Rust has a stable ABI on where the data* is in the function arguments (presumably first?), you don't need to do anything if it matches the C code's expected function signature including the user context arg.
Unfortunately a lot of existing C APIs won't have the user arg in the place you need it, it's a mix of first, last, and sometimes even middle.
Yes but my problem wasn’t with the user data pointer but the fact that I needed a STATIC generic lambda. Static because the C library then forks and continues to call the lambda in the new process but I also type based conversions in it.
Programming in a spreadsheet is an anti-pattern. Does anyone review your workflow? Write tests for it? Use a real programming language; a notebook at least.
Streamlit apps or similar are doing a great job at this where I'm at.
As simple to build and deploy as Excel, but with the right data types, the right UI, the right access and version control, the right programming language that LLMs understand, the right SW ecosystem and packages, etc.
reply