Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I 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.


I read the code but I'm not sure I understood all of it (I'm familiar with Elixir, not with Gleam).

For normal matters I do believe that your approach works but (start returns the pid of the server, right?) what is it going to happen if something, probably a module written in Elixir or Erlang that wants to prove a point, sends a message of an unsupported type to that pid? I don't think the compiler can prevent that. It's going to crash at runtime or have to handle the unmatched type and return a not implemented sort of error.

It's similar to static typing a JSON API, then receiving an odd message from the server or from the client, because the remote party cannot be controlled.


> [...] start returns the pid of the server, right?

Yes, `start` is the part you would stick in a supervision tree, essentially. We start the server so that it can be reached later with the interface functions.

> [...] probably a module written in Elixir or Erlang that wants to prove a point, sends a message of an unsupported type to that pid? I don't think the compiler can prevent that. It's going to crash at runtime or have to handle the unmatched type and return a not implemented sort of error.

Yes, this is already the default behavior of a `gen_server` and is fine, IMO. As a general guideline I would advise against trying to fix errors caused by type-unsafe languages; there is no productive (i.e. long-term fruitful) way to fix a fundamentally unsafe interface (Erlang/Elixir code), the best recourse you have is to write as much code you can in the safe one instead.

Erlang, in Gleam code, is essentially a layer where you put the code that does the fundamentals and then you use the foreign function interface (FFI) to tell Gleam that those functions can be called with so and so types, and it does the type checking. This means that once you travel into Erlang code all bets are off. It's really no different to saying that a certain C function can call assembly code.

    pub type ProcessReference(message, term) {
      ByPid(Pid(message))
      ByGlobal(term)
      ByLocal(Atom)
    }
    
    @external(erlang, "otp_server", "call")
    fn gen_server_call(
      pid: ProcessReference(message, term),
      call_func: CallFunc(state, reply),
    ) -> Result(reply, Nil)
And the corresponding Erlang code:

    call({by_pid, Pid}, CallFunc) ->
        {ok, gen_server:call(Pid, {call_by_func, CallFunc})};
    call({by_global, Name}, CallFunc) ->
        {ok, gen_server:call({global, Name}, {call_by_func, CallFunc})};
    call({by_local, Name}, CallFunc) ->
        {ok, gen_server:call(Name, {call_by_func, CallFunc})}.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: