Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better typing for trapped exits and monitor messages #20

Open
CrowdHailer opened this issue May 17, 2020 · 5 comments
Open

Better typing for trapped exits and monitor messages #20

CrowdHailer opened this issue May 17, 2020 · 5 comments

Comments

@CrowdHailer
Copy link
Contributor

CrowdHailer commented May 17, 2020

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)
  • Calling this function sets the flag in the process.
  • receive is only passed in to get the message type correct.
  • the mapping function can be stored in the process dictionary
  • this API should only be called once, which cannot be enforced except by a runtime check.
Option 2
process.spawn_link(fn(receive, trap_exit) {
  trap_exit(fn(pid, reason) { OhDear(pid, reason) })

  // continue ...
})
  • does not need receive to be passed to the trap_exit function.
  • Still needs to be called only once, cannot be enforced.
Option 3
process.spawn_link(fn(receive) {
  // continue ...
    
}, Ok(fn(pid, reason) { Exit(pid, reason) }))
  • No possibility of setting trap exit twice.
  • Increased Noise to start a process without trapping exits, need to pass an Error as configuration argument. see below
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) })
@CrowdHailer CrowdHailer changed the title Better typing for trap exits Better typing for trapped exits and monitor messages May 17, 2020
@lpil
Copy link

lpil commented May 17, 2020

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link

lpil commented May 20, 2020

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

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

@lpil
Copy link

lpil commented May 20, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants