midas-framework / midas

A framework for Gleam, Midas makes shiny things.
https://hex.pm/packages/midas
190 stars 5 forks source link

Better typing for trapped exits and monitor messages #20

Open CrowdHailer opened 4 years ago

CrowdHailer commented 4 years ago

See this Elixir forum post for my explination of typed processes up to this point.

Background

  1. An important difference between links and monitor is that an exit message can come from a Pid that it didn't know about, for example the parent process that used start_link. A monitor message will always come from a monitor the process it's self instantiated.
  2. Processes define their own mailbox message type, this means that process's sending to a mailbox must know the type. In the case of pub sub when the pubsub broker might have may types of clients connected the client should subscribe with a translate function that is used to cast the published update to an acceptable message type. I think the same principle can be applied to trapping exits and handling monitors. To set up either monitor/link the process must indicate how it will handle those messages.

Exits.

A process traps exits by providing the function that will handle the message. Start with a process with the following message type

pub type Message {
  Foo
  Bar
  OhDear(BarePid, Dynamic)
}

The Exit type is OhDear to make clear its a type defined by the client, it would I expect normally be called exit in practice.

Option 1
process.spawn_link(fn(receive) {
  process.trap_exit(receive, fn(pid, reason) { OhDear(pid, reason) })

  // continue ...
})

Signature of trap_exit function.

fn trap_exit(fn(Wait) -> m, fn(BarePid, Reason) -> m)
Option 2
process.spawn_link(fn(receive, trap_exit) {
  trap_exit(fn(pid, reason) { OhDear(pid, reason) })

  // continue ...
})
Option 3
process.spawn_link(fn(receive) {
  // continue ...

}, Ok(fn(pid, reason) { Exit(pid, reason) }))
process.spawn_link(fn(receive) {
  // continue ...

}, Error(Nil))

Monitors

Very similar except the mapping function is passed when the monitor is created.

process.monitor(pid, fn(pid, reason) { MyDown(pid) })
lpil commented 4 years ago

Thanks for sharing! This is very cool :)

This is the same design that the gleam-experiments/otp_process library currently uses: https://github.com/gleam-experiments/otp_process/blob/02f8ba0b5b3c3c39096ad7117f9a486e6c5281eb/src/gleam/otp/process.gleam#L141

Still needs to be called only once, cannot be enforced.

What is the problem with calling it twice? It could potentially change the mapping function, but I don't see a problem with that? Perhaps I'm missing something.

Option 2

This is not type safe as the msg transformer function could return a type different to that which the process accepts as a message.

Very similar except the mapping function is passed when the monitor is created.

Does this mean the process will hold a map of functions that take pids/ports and return a message? I think we'd need to remove the function from the map once the that down message has been received to prevent a memory leak.

CrowdHailer commented 4 years ago

What is the problem with calling it twice? It could potentially change the mapping function, but I don't see a problem with that? Perhaps I'm missing something.

Maybe it isn't a problem, although I can't see a reason to change the mapping function.

This is not type safe as the msg transformer function could return a type different to that which the process accepts as a message.

You can type the spawn function to make sure they have to be the same return type. For example this produces a compiler error

fn myfunc(foo: fn() -> a, bar: fn() -> a) {
    todo
}

fn demo() {
    myfunc(fn() { 2 }, fn() { 2.0 })
}

Does this mean the process will hold a map of functions that take pids/ports and return a message?

Basically yes, although I would have the key of the map be the reference from the monitor, it's possible to have more than one monitor point to the same pid

CrowdHailer commented 4 years ago

I think there is an option 4.

process.spawn_link(fn(receive) {
  // continue ...

}, [TrapExit(fn(pid, reason) { Exit(pid, reason) })])

Then if you are not trapping exits you can just put an empty list.

process.spawn_link(fn(receive) {
  // continue ...

}, [])

This is much neater than the Error(Nil) as last argument. Also other process flags and spawn options could be passed in that list.

p.s. this is my new favourite approach

lpil commented 4 years ago

That is 100% what the my OTP process module does 😁

(Or did, I'm currently fiddling with it)

lpil commented 4 years ago

https://github.com/gleam-lang/otp/blob/607b5a8e45727999f07438ef3f81053aa6a61cfb/test/gleam/otp/process_test.gleam#L117-L127

Looks like it was a while back, but still! I like this design.