Skip to content

Proposal: Mapped names #93

@sbergen

Description

@sbergen

Problem

Currently, named subjects are restrictive in the sense that they can not be used in the following scenario using named processes (usually with supervision):

  • Process 1 is running code that has a function subscribe(source: Source1, subject: Subject(Msg1))
  • Process 2 is running code that has a function subscribe(source: Srouce2, subject: Subject(Msg2))
  • Process 3 wants to subscribe to both with a named subject.

While with unnamed subjects, we can do

let subject1 = process.new_subject()
let subject2 = process.new_subject()

mod1.subscribe(source1, subject1)
mod2.subscribe(source2, subject2)

process.new_selector()
|> process.select_map(subject1, map_msg_1)
|> process.select_map(subject1, map_msg_2)

the same does not work with named subjects.

Proposal

Instead, I'm proposing a mechanism to create a set of names that have a similar data mapping built into them. Here is a test from my proof of concept branch:

pub fn mapped_name_test() {
  let #(name1, name2, compound_name) =
    process.new_mapped_names2("test", int.to_string, float.to_string)
  let sync = process.new_subject()

  process.spawn(fn() {
    assert process.register(process.self(), compound_name) == Ok(Nil)
    process.send(sync, False)

    let subject = process.named_subject(compound_name)

    // Expect the mapped messages
    assert process.receive(subject, 100) == Ok("1")
    assert process.receive(subject, 100) == Ok("2.0")
    assert process.receive(subject, 100) == Ok("3")
    process.send(sync, True)
  })

  // Wait for name to be registered
  assert process.receive(sync, 10) == Ok(False)

  // Send the messages
  process.send(process.named_subject(name1), 1)
  process.send(process.named_subject(name2), 2.0)
  process.send(process.named_subject(compound_name), "3")

  assert process.receive(sync, 100) == Ok(True)
}

Consequences

Names are no longer always atoms

This would inherently have the consequence of process names no longer being atoms, when created like this, but rather carry some data along with them. I could not think of a way to have only the "combining" name carry extra data. My current PoC takes the opposite approach and only adds data to the "sending" names, but has the downside that the mapping function (which might close over data) is sent alongside each message.

Another approach that could work instead, is tagging the messages that require mapping with an atom or integer, and store a map of mapping functions in the "combining" name, so that it would need to be moved to the receiving process only once. However, this would add more complexity to the implementation (and one map lookup per receive).

The third option is to do the mapping in the sending process, but I don't like moving the potential for panicking to the sender. However, it would make the solution more lightweight.

I considered some other options also, but didn't find anything I'd like more than these three.

Multiple new_mapped_namesX functions

Given Gleam doesn't have variadics, I couldn't think of a type safe way to do this, without having to write multiple arities of a function like this.

Questions

Did I miss some scenario where this would not work? Is doing this well too complex to justify the benefit? Does this look like a proposal worth pursuing further? If so, are the following principles something we should stick to, or are some of them negotiable?

  1. The mapping function should be run on the receiving process.
  2. We should not be sending functions alongside the messages, as they could cause copying of closed over data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions