3 ms·
Interesting! I have a F# background, and thought to have read that some constructs I learned to appreciate are not available in Gleam (the one I can think of r
by raphinou 1y ago
Interesting!
I have a F# background, and thought to have read that some constructs I learned to appreciate are not available in Gleam (the one I can think of right now is currying, but I thought there were others).
Also, Gleam otp didn't seem to be a priority.
What's your experience regarding these 2 points?
- innocentoldguy 1y agoThe issue isn't that OTP isn't a priority for Gleam, but rather that it doesn't work with the static typing Gleam is implementing. This is why they've had to reimplement their own OTP functionality in gleam_otp. Even then, gleam_otp has some limitations, like being unable to support all of OTP's messages, named processes, etc. gleam_otp is also considered experimental at this point.
- 59nadir 1y agoHaving Erlang-style OTP support (for the most part) is very doable, I've written my own OTP layer instead of the pretty shoddy stuff Gleam ships with. It's not really that challenging of a problem and you can get stuff like typed processes (`Pid(message_type)`, i.e. we can only send `message_type` messages to this process), etc. out of it very easily. This idea that static typing is such a massive issue for OTP style servers and messaging is a very persistent myth, to be honest; I've created thin layers on top of OTP for both `purerl` (PureScript compiled to Erlang) and Gleam that end up with both type-safe interfaces (we can only send the right messages to the processes) and are type-safe internally (we can only write the process in a type-safe way based on its state and message types).
- innocentoldguy 1y agoI wholeheartedly agree with you that gleam_otp is janky. Still, actor message passing is only part of the picture. Here are some issues that make static typing difficult in OTP: • OTP processes communicate via the actor model by sending messages of any type. Each actor is responsible for pattern-matching the incoming message and handling it (or not) based on its type. To implement static typing, you need to know at compile time what type of message an actor can receive, what type it will send back, and how to verify this at compile time. • OTP's GenServer behaviour uses callbacks that can return various types, depending on runtime conditions. Static typing would require that you predefine all return types for all callbacks, handle type-safe state management, and provide compile-time guarantees when handling these myriad types. • OTP supervisors manage child processes dynamically, which could be of any type. To implement static typing, you would need to know and define the types of all supervised processes, know how they are going to interact with each other, and implement type-safe restart strategies for each type. These and other design roadblocks may be why Gleam chose to implement primitives, like statically typed actors, instead of GenServer, GenStage, GenEvent, and other specialized OTP behaviours, full supervisor functionality, DynamicSupervisor, and OTP's Registry, Agent, Task, etc. OTP and BEAM are Erlang and Elixir's killer features, and have been battle-tested in some of the most demanding environments for decades. I can't see the logic in ditching them or cobbling together a lesser, unproven version of them to gain something as mundane as static typing. EDIT: I completely missed the word "actor" as the second word in my second sentence, so I added it.
- 59nadir 1y agoI suppose I was unclear. It is OTP-style `gen_server` processes that I'm talking about. > OTP processes communicate via the actor model by sending messages of any type. Each actor is responsible for pattern-matching the incoming message and handling it (or not) based on its type. To implement static typing, you need to know at compile time what type of message an actor can receive, what type it will send back, and how to verify this at compile time. This is trivial, your `start` function can simply take a function that says which type of message you can receive. Better yet, you split it up in `handle_cast` (which has a well known set of valid return values, you type that as `incomingCastType -> gen_server.CastReturn`) and deal with the rest with interface functions just as you would in normal Erlang usage (i.e. `get_user_preferences(user_preference_process_pid) -> UserPreferences` at the top level of the server). Here is an example of a process I threw together having never used Gleam before. The underlying `gen_server` library is my own as well, as well as the FFI code (Erlang code) that backs it. My point with posting this is mostly that all of the parts of the server, i.e. what you define what you define a server, are type safe in the type of way that people claim is somehow hard: import gleam/option import otp/gen_server import otp/types.{type Pid} pub type State { State(count: Int, initial_count: Int) } pub type Message { Increment(Int) Decrement(Int) Reset } pub fn start(initial_count: Int) -> Result(Pid(Message), Nil) { let spec = gen_server.GenServerStartSpec( handle_cast:, init:, name: option.Some(gen_server.GlobalName("counter")), ) gen_server.start(spec, initial_count) } fn name() -> gen_server.ProcessReference(Message, String) { gen_server.ByGlobal("counter") } pub fn count() -> Result(Int, Nil) { gen_server.call(name(), fn(state: State) -> gen_server.CallResult(State, Int) { gen_server.CallOk(new_state: state, reply: state.count) }) } pub fn increment(count: Int) -> Nil { gen_server.cast(name(), Increment(count)) } pub fn decrement(count: Int) -> Nil { gen_server.cast(name(), Decrement(count)) } pub fn reset() -> Nil { gen_server.cast(name(), Reset) } pub fn init(initial_count: Int) -> State { State(count: initial_count, initial_count:) } pub fn handle_cast( message: Message, state: State, ) -> gen_server.CastResult(State) { case message { Increment(count) -> gen_server.CastOk(State(..state, count: state.count + count)) Decrement(count) -> gen_server.CastOk(State(..state, count: state.count - count)) Reset -> gen_server.CastOk(State(..state, count: state.initial_count)) } } It's not nearly as big of an issue as people make it out to be; most of the expected behaviors are exactly that: `behaviour`s, and they're not nearly as dynamic as people make them seem. Gleam itself maps custom types very cleanly to tagged tuples (`ThingHere("hello")` maps to `{thing_here, <<"hello">>}`, and so on) so there is no real big issue with mapping a lot of the known and useful return types and so on.